/*-------------------------------------------------------------------------
 *
 * heapam.c
 *	  堆访问方法代码
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/heap/heapam.c
 *
 *
 * INTERFACE ROUTINES
 *		heap_beginscan	- 开始关系扫描
 *		heap_rescan		- 重新开始关系扫描
 *		heap_endscan	- 结束关系扫描
 *		heap_getnext	- 检索扫描中的下一个元组
 *		heap_fetch		- 检索具有给定tid的元组
 *		heap_insert		- 将元组插入关系中
 *		heap_multi_insert - 将多个元组插入关系中
 *		heap_delete		- 从关系中删除元组
 *		heap_update		- 用另一个元组替换关系中的元组
 *
 * NOTES
 *	  本文件包含实现所有POSTGRES关系的POSTGRES堆访问方法的heap_例程。
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/bufmask.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "access/heapam_xlog.h"
#include "access/heaptoast.h"
#include "access/hio.h"
#include "access/multixact.h"
#include "access/parallel.h"
#include "access/relscan.h"
#include "access/subtrans.h"
#include "access/syncscan.h"
#include "access/sysattr.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/valid.h"
#include "access/visibilitymap.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
#include "catalog/catalog.h"
#include "catalog/pg_database.h"
#include "catalog/pg_database_d.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/atomics.h"
#include "port/pg_bitutils.h"
#include "storage/bufmgr.h"
#include "storage/freespace.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
#include "storage/procarray.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "storage/standby.h"
#include "utils/datum.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/relcache.h"
#include "utils/snapmgr.h"
#include "utils/spccache.h"
#include "utils/syscache.h"


static HeapTuple fc_heap_prepare_insert(Relation fc_relation, HeapTuple fc_tup,
									 TransactionId fc_xid, CommandId fc_cid, int fc_options);
static XLogRecPtr fc_log_heap_update(Relation fc_reln, Buffer fc_oldbuf,
								  Buffer fc_newbuf, HeapTuple fc_oldtup,
								  HeapTuple fc_newtup, HeapTuple fc_old_key_tuple,
								  bool fc_all_visible_cleared, bool fc_new_all_visible_cleared);
#ifdef USE_ASSERT_CHECKING
static void fc_check_lock_if_inplace_updateable_rel(Relation fc_relation,
												 ItemPointer fc_otid,
												 HeapTuple fc_newtup);
static void fc_check_inplace_rel_lock(HeapTuple fc_oldtup);
#endif
static Bitmapset *fc_HeapDetermineColumnsInfo(Relation fc_relation,
										   Bitmapset *fc_interesting_cols,
										   Bitmapset *fc_external_cols,
										   HeapTuple fc_oldtup, HeapTuple fc_newtup,
										   bool *fc_has_external);
static bool fc_heap_acquire_tuplock(Relation fc_relation, ItemPointer fc_tid,
								 LockTupleMode fc_mode, LockWaitPolicy fc_wait_policy,
								 bool *fc_have_tuple_lock);
static void fc_compute_new_xmax_infomask(TransactionId fc_xmax, uint16 fc_old_infomask,
									  uint16 fc_old_infomask2, TransactionId fc_add_to_xmax,
									  LockTupleMode fc_mode, bool fc_is_update,
									  TransactionId *fc_result_xmax, uint16 *fc_result_infomask,
									  uint16 *fc_result_infomask2);
static TM_Result fc_heap_lock_updated_tuple(Relation fc_rel, HeapTuple fc_tuple,
										 ItemPointer fc_ctid, TransactionId fc_xid,
										 LockTupleMode fc_mode);
static void fc_GetMultiXactIdHintBits(MultiXactId fc_multi, uint16 *fc_new_infomask,
								   uint16 *fc_new_infomask2);
static TransactionId fc_MultiXactIdGetUpdateXid(TransactionId fc_xmax,
											 uint16 fc_t_infomask);
static bool fc_DoesMultiXactIdConflict(MultiXactId fc_multi, uint16 fc_infomask,
									LockTupleMode fc_lockmode, bool *fc_current_is_member);
static void fc_MultiXactIdWait(MultiXactId fc_multi, MultiXactStatus fc_status, uint16 fc_infomask,
							Relation fc_rel, ItemPointer fc_ctid, XLTW_Oper fc_oper,
							int *fc_remaining);
static bool fc_ConditionalMultiXactIdWait(MultiXactId fc_multi, MultiXactStatus fc_status,
									   uint16 fc_infomask, Relation fc_rel, int *fc_remaining);
static void fc_index_delete_sort(TM_IndexDeleteOp *fc_delstate);
static int	fc_bottomup_sort_and_shrink(TM_IndexDeleteOp *fc_delstate);
static XLogRecPtr fc_log_heap_new_cid(Relation fc_relation, HeapTuple fc_tup);
static HeapTuple fc_ExtractReplicaIdentity(Relation fc_rel, HeapTuple fc_tup, bool fc_key_required,
										bool *fc_copy);


/*
 * 每个元组锁模式都有一个相应的重量级锁，以及一个或两个相应的 MultiXactStatuses（一个仅用于锁定元组，另一个用于更新它们）。这个表（以及下面的宏）帮助我们确定用于任何特定元组锁定强度的重量级锁模式和 MultiXactStatus 值。
 *
 * 这些与 InplaceUpdateTupleLock 交互，它是 ExclusiveLock 的别名。
 *
 * 不要直接查看 lockstatus/updstatus！请使用 get_mxact_status_for_lock。
 */
static const struct
{
	LOCKMODE	hwlock;
	int			lockstatus;
	int			updstatus;
}

			tupleLockExtraInfo[MaxLockTupleMode + 1] =
{
	{							/* LockTupleKeyShare */
		AccessShareLock,
		MultiXactStatusForKeyShare,
		-1						/* KeyShare 不允许更新元组 */
	},
	{							/* LockTupleShare */
		RowShareLock,
		MultiXactStatusForShare,
		-1						/* Share 不允许更新元组 */
	},
	{							/* LockTupleNoKeyExclusive */
		ExclusiveLock,
		MultiXactStatusForNoKeyUpdate,
		MultiXactStatusNoKeyUpdate
	},
	{							/* LockTupleExclusive */
		AccessExclusiveLock,
		MultiXactStatusForUpdate,
		MultiXactStatusUpdate
	}
};

/* 获取给定 MultiXactStatus 的 LOCKMODE */
#define LOCKMODE_from_mxstatus(status) \
			(tupleLockExtraInfo[TUPLOCK_from_mxstatus((status))].hwlock)

/*
 * 使用 LockTupleMode 强度值获取元组上的重量级锁。
 * 这比让每个调用者将其翻译为 lock.h 的 LOCKMODE 更具可读性。
 */
#define LockTupleTuplock(rel, tup, mode) \
	LockTuple((rel), (tup), tupleLockExtraInfo[mode].hwlock)
#define UnlockTupleTuplock(rel, tup, mode) \
	UnlockTuple((rel), (tup), tupleLockExtraInfo[mode].hwlock)
#define ConditionalLockTupleTuplock(rel, tup, mode) \
	ConditionalLockTuple((rel), (tup), tupleLockExtraInfo[mode].hwlock)

#ifdef USE_PREFETCH
/*
 * heap_index_delete_tuples 和 index_delete_prefetch_buffer 使用此
 * 结构来协调预取活动
 */
typedef struct
{
	BlockNumber cur_hblkno;
	int			next_item;
	int			ndeltids;
	TM_IndexDelete *deltids;
} IndexDeletePrefetchState;
#endif

/* heap_index_delete_tuples 自下而上的索引删除成本常数 */
#define BOTTOMUP_MAX_NBLOCKS			6
#define BOTTOMUP_TOLERANCE_NBLOCKS		3

/*
 * heap_index_delete_tuples 在确定必须访问哪些堆块
 * 以帮助其自下而上的索引删除调用者时使用此内容
 */
typedef struct IndexDeleteCounts
{
	int16		npromisingtids; /* 组中“有前途”的 TID 数量 */
	int16		ntids;			/* 组中 TID 的数量 */
	int16		ifirsttid;		/* 至组的第一个 deltid 的偏移量 */
} IndexDeleteCounts;

/*
 * 此表映射每个特定 MultiXactStatus 值的元组锁强度值。
 */
static const int MultiXactStatusLock[MaxMultiXactStatus + 1] =
{
	LockTupleKeyShare,			/* ForKeyShare */
	LockTupleShare,				/* ForShare */
	LockTupleNoKeyExclusive,	/* ForNoKeyUpdate */
	LockTupleExclusive,			/* ForUpdate */
	LockTupleNoKeyExclusive,	/* NoKeyUpdate */
	LockTupleExclusive			/* Update */
};

/* 获取给定 MultiXactStatus 的 LockTupleMode */
#define TUPLOCK_from_mxstatus(status) \
			(MultiXactStatusLock[(status)])

/* ----------------------------------------------------------------
 *                          heap 支持例程
 * ----------------------------------------------------------------
 */

/* ----------------
 *      initscan - heap_beginscan 和 heap_rescan 公共的扫描代码
 * ----------------
 */
static void fc_initscan(HeapScanDesc fc_scan, ScanKey fc_key, bool fc_keep_startblock)
{
	ParallelBlockTableScanDesc fc_bpscan = NULL;
	bool		fc_allow_strat;
	bool		fc_allow_sync;

	/*
	 * 确定我们必须扫描的块数。
	 *
	 * 在扫描开始时执行此操作就足够了，因为在扫描进行时添加的任何元组
	 * 对我的快照来说都是不可见的。
	 * （使用非 MVCC 快照时情况并非如此。然而，我们无法
	 * 保证返回扫描开始后添加的元组，因为它们可能会进入我们已经扫描的页面。
	 * 为了保证非 MVCC 快照的一致结果，调用者必须持有某种更高级别的
	 * 锁，以确保有趣的元组不会改变。）
	 */
	if (fc_scan->rs_base.rs_parallel != NULL)
	{
		fc_bpscan = (ParallelBlockTableScanDesc) fc_scan->rs_base.rs_parallel;
		fc_scan->rs_nblocks = fc_bpscan->phs_nblocks;
	}
	else
		fc_scan->rs_nblocks = RelationGetNumberOfBlocks(fc_scan->rs_base.rs_rd);

	/*
	 * 如果表相对于 NBuffers 来说很大，请使用批量读取访问策略并启用同步扫描（参见 syncscan.c）。虽然这些特性的阈值可能不同，但我们将它们设置为相同，这样只需调整两个行为而不是四个。 （但是，一些调用者需要能够独立于表的大小禁用一个或两个这些行为；此外，还有一个 GUC 变量可以禁用同步扫描。）
	 *
	 * 请注意，table_block_parallelscan_initialize 有一个非常相似的测试； 如果您更改这个，考虑同时更改那个。 
	 */
	if (!RelationUsesLocalBuffers(fc_scan->rs_base.rs_rd) &&
		fc_scan->rs_nblocks > NBuffers / 4)
	{
		fc_allow_strat = (fc_scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
		fc_allow_sync = (fc_scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
	}
	else
		fc_allow_strat = fc_allow_sync = false;

	if (fc_allow_strat)
	{
		/* 在重新扫描期间，保持先前的策略对象。 */
		if (fc_scan->rs_strategy == NULL)
			fc_scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
	}
	else
	{
		if (fc_scan->rs_strategy != NULL)
			FreeAccessStrategy(fc_scan->rs_strategy);
		fc_scan->rs_strategy = NULL;
	}

	if (fc_scan->rs_base.rs_parallel != NULL)
	{
		/* 对于并行扫描，相信 ParallelTableScanDesc 所说的内容。 */
		if (fc_scan->rs_base.rs_parallel->phs_syncscan)
			fc_scan->rs_base.rs_flags |= SO_ALLOW_SYNC;
		else
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC;
	}
	else if (fc_keep_startblock)
	{
		/*
		 * 在重新扫描时，我们希望保持先前的 startblock 设置，
		 * 这样重绕光标就不会产生意外的结果。
		 * 不过，重置活跃的同步扫描设置。 
		 */
		if (fc_allow_sync && synchronize_seqscans)
			fc_scan->rs_base.rs_flags |= SO_ALLOW_SYNC;
		else
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC;
	}
	else if (fc_allow_sync && synchronize_seqscans)
	{
		fc_scan->rs_base.rs_flags |= SO_ALLOW_SYNC;
		fc_scan->rs_startblock = ss_get_location(fc_scan->rs_base.rs_rd, fc_scan->rs_nblocks);
	}
	else
	{
		fc_scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC;
		fc_scan->rs_startblock = 0;
	}

	fc_scan->rs_numblocks = InvalidBlockNumber;
	fc_scan->rs_inited = false;
	fc_scan->rs_ctup.t_data = NULL;
	ItemPointerSetInvalid(&fc_scan->rs_ctup.t_self);
	fc_scan->rs_cbuf = InvalidBuffer;
	fc_scan->rs_cblock = InvalidBlockNumber;

	/* page-at-a-time 字段在未初始化 rs_inited 时始终无效 */

	/*
	 * 如果合适，复制扫描键
	 */
	if (fc_key != NULL && fc_scan->rs_base.rs_nkeys > 0)
		memcpy(fc_scan->rs_base.rs_key, fc_key, fc_scan->rs_base.rs_nkeys * sizeof(ScanKeyData));

	/*
	 * 目前，我们只对顺序堆扫描有统计计数器（例如，对于位图扫描，底层位图索引扫描将被计算，而对于样本扫描，我们更新元组获取的统计信息）。
	 */
	if (fc_scan->rs_base.rs_flags & SO_TYPE_SEQSCAN)
		pgstat_count_heap_scan(fc_scan->rs_base.rs_rd);
}

/*
 * heap_setscanlimits - 限制堆扫描的范围
 *
 * startBlk 是要开始的页面
 * numBlks 是要扫描的页面数量（InvalidBlockNumber 意味着“全部”）
 */
void heap_setscanlimits(TableScanDesc fc_sscan, BlockNumber fc_startBlk, BlockNumber fc_numBlks)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;

	Assert(!fc_scan->rs_inited);	/* 否则太晚无法更改 */
	/* 否则 rs_startblock 是重要的 */
	Assert(!(fc_scan->rs_base.rs_flags & SO_ALLOW_SYNC));

	/* 检查 startBlk 是否有效（但允许零块的情况...） */
	Assert(fc_startBlk == 0 || fc_startBlk < fc_scan->rs_nblocks);

	fc_scan->rs_startblock = fc_startBlk;
	fc_scan->rs_numblocks = fc_numBlks;
}

/*
 * heapgetpage - heapgettup() 的子例程
 *
 * 该例程读取并固定关系的指定页面。
 * 在逐页处理模式下，它执行额外的工作，即确定页面上哪些元组是可见的。
 */
void heapgetpage(TableScanDesc fc_sscan, BlockNumber fc_page)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;
	Buffer		fc_buffer;
	Snapshot	fc_snapshot;
	Page		fc_dp;
	int			fc_lines;
	int			fc_ntup;
	OffsetNumber fc_lineoff;
	ItemId		fc_lpp;
	bool		fc_all_visible;

	Assert(fc_page < fc_scan->rs_nblocks);

	/* 释放先前的扫描缓冲区（如果有） */
	if (BufferIsValid(fc_scan->rs_cbuf))
	{
		ReleaseBuffer(fc_scan->rs_cbuf);
		fc_scan->rs_cbuf = InvalidBuffer;
	}

	/*
	 * 确保每页至少检查一次中断。 在更高代码级别的检查无法停止遇到许多连续死元组的 seqscan。
	 */
	CHECK_FOR_INTERRUPTS();

	/* 使用选择的策略读取页面 */
	fc_scan->rs_cbuf = ReadBufferExtended(fc_scan->rs_base.rs_rd, MAIN_FORKNUM, fc_page,
									   RBM_NORMAL, fc_scan->rs_strategy);
	fc_scan->rs_cblock = fc_page;

	if (!(fc_scan->rs_base.rs_flags & SO_ALLOW_PAGEMODE))
		return;

	fc_buffer = fc_scan->rs_cbuf;
	fc_snapshot = fc_scan->rs_base.rs_snapshot;

	/*
	 * 如果可能，修剪并修复整个页面的碎片。
	 */
	heap_page_prune_opt(fc_scan->rs_base.rs_rd, fc_buffer);

	/*
	 * 在检查元组可见性时，必须对缓冲区内容持有共享锁。 然而，之后，只要我们保持缓冲区固定，我们找到的可见元组就保证是好的。
	 */
	LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);

	fc_dp = BufferGetPage(fc_buffer);
	TestForOldSnapshot(fc_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
	fc_lines = PageGetMaxOffsetNumber(fc_dp);
	fc_ntup = 0;

	/*
	 * 如果 all-visible 标志表明页面上的所有元组对所有人都是可见的，
	 * 我们可以跳过每个元组的可见性测试。
	 *
	 * 注意：在热备用中，已经对主库上的所有事务可见的元组
	 * 仍然可能对备用中的只读事务不可见。我们通过跟踪
	 * 可见元组的最小 xmin 作为截止 XID 来部分处理此问题，
	 * 同时在主库上标记一个页面为 all-visible，并将其与
	 * 可见性映射 SET 操作一起记录在 WAL 日志中。在热备用中，
	 * 我们等待（或中止）所有可能无法看到页面上一个或多个元组的
	 * 事务。这就是索引仅扫描在热备用中工作的原因。索引仅扫描和
	 * 堆扫描之间一个关键的区别是，索引仅扫描完全依赖于可见性映射，
	 * 而堆扫描查看的是页面级的 PD_ALL_VISIBLE 标志。我们不确定
	 * 页面级标志是否可以以相同的方式被信任，因为它可能以某种方式
	 * 被传播而没有被显式的 WAL 记录，例如通过完全页面写入。
	 * 在我们能够毫无疑问地证明这一点之前，让我们以困难的方式检查
	 * 每个元组的可见性。
	 */
	fc_all_visible = PageIsAllVisible(fc_dp) && !fc_snapshot->takenDuringRecovery;

	for (fc_lineoff = FirstOffsetNumber, fc_lpp = PageGetItemId(fc_dp, fc_lineoff);
		 fc_lineoff <= fc_lines;
		 fc_lineoff++, fc_lpp++)
	{
		if (ItemIdIsNormal(fc_lpp))
		{
			HeapTupleData fc_loctup;
			bool		fc_valid;

			fc_loctup.t_tableOid = RelationGetRelid(fc_scan->rs_base.rs_rd);
			fc_loctup.t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lpp);
			fc_loctup.t_len = ItemIdGetLength(fc_lpp);
			ItemPointerSet(&(fc_loctup.t_self), fc_page, fc_lineoff);

			if (fc_all_visible)
				fc_valid = true;
			else
				fc_valid = HeapTupleSatisfiesVisibility(&fc_loctup, fc_snapshot, fc_buffer);

			HeapCheckForSerializableConflictOut(fc_valid, fc_scan->rs_base.rs_rd,
												&fc_loctup, fc_buffer, fc_snapshot);

			if (fc_valid)
				fc_scan->rs_vistuples[fc_ntup++] = fc_lineoff;
		}
	}

	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

	Assert(fc_ntup <= MaxHeapTuplesPerPage);
	fc_scan->rs_ntuples = fc_ntup;
}

/* ----------------
 *		heapgettup - 获取下一个堆元组
 *
 *		如果尚未初始化扫描，则进行初始化；然后根据“dir”指示
 *		前进到下一个元组；将下一个元组返回到 scan->rs_ctup,
 *		如果没有更多元组，则将 scan->rs_ctup.t_data = NULL。
 *
 * dir == NoMovementScanDirection 表示“重新获取 scan->rs_ctup 指示的元组”。
 *
 * 注意：虽然 nkeys/key 被单独传递，尽管它们保存在扫描描述符中，
 * 但这是因为调用者可能不希望我们检查 scankeys。
 *
 * 注意：当我们在任一方向上跌落扫描末尾时，我们重置 rs_inited。
 * 这意味着进一步的请求与相同扫描方向将重新启动扫描，这有点奇怪，
 * 但是与相反扫描方向的请求将启动一个新的扫描
 * 向正确方向进行。这是游标所需的行为，而前一种情况在 Postgres
 * 中通常是未定义的行为，所以我们不太在乎。
 * ----------------
 */
static void fc_heapgettup(HeapScanDesc fc_scan,
		   ScanDirection fc_dir,
		   int fc_nkeys,
		   ScanKey fc_key)
{
	HeapTuple	fc_tuple = &(fc_scan->rs_ctup);
	Snapshot	fc_snapshot = fc_scan->rs_base.rs_snapshot;
	bool		fc_backward = ScanDirectionIsBackward(fc_dir);
	BlockNumber fc_page;
	bool		fc_finished;
	Page		fc_dp;
	int			fc_lines;
	OffsetNumber fc_lineoff;
	int			fc_linesleft;
	ItemId		fc_lpp;

	/*
	 * 根据扫描方向计算下一个起始 lineoff
	 */
	if (ScanDirectionIsForward(fc_dir))
	{
		if (!fc_scan->rs_inited)
		{
			/*
			 * 如果关系为空，则立即返回 null
			 */
			if (fc_scan->rs_nblocks == 0 || fc_scan->rs_numblocks == 0)
			{
				Assert(!BufferIsValid(fc_scan->rs_cbuf));
				fc_tuple->t_data = NULL;
				return;
			}
			if (fc_scan->rs_base.rs_parallel != NULL)
			{
				ParallelBlockTableScanDesc fc_pbscan =
				(ParallelBlockTableScanDesc) fc_scan->rs_base.rs_parallel;
				ParallelBlockTableScanWorker fc_pbscanwork =
				fc_scan->rs_parallelworkerdata;

				table_block_parallelscan_startblock_init(fc_scan->rs_base.rs_rd,
														 fc_pbscanwork, fc_pbscan);

				fc_page = table_block_parallelscan_nextpage(fc_scan->rs_base.rs_rd,
														 fc_pbscanwork, fc_pbscan);

				/* 其他进程可能已经完成了扫描。 */
				if (fc_page == InvalidBlockNumber)
				{
					Assert(!BufferIsValid(fc_scan->rs_cbuf));
					fc_tuple->t_data = NULL;
					return;
				}
			}
			else
				fc_page = fc_scan->rs_startblock; /* 第一页 */
			heapgetpage((TableScanDesc) fc_scan, fc_page);
			fc_lineoff = FirstOffsetNumber;	/* 第一个 offnum */
			fc_scan->rs_inited = true;
		}
		else
		{
			/* 从先前返回的页面/元组继续 */
			fc_page = fc_scan->rs_cblock; /* 当前页面 */
			fc_lineoff =			/* 下一个 offnum */
				OffsetNumberNext(ItemPointerGetOffsetNumber(&(fc_tuple->t_self)));
		}

		LockBuffer(fc_scan->rs_cbuf, BUFFER_LOCK_SHARE);

		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lines = PageGetMaxOffsetNumber(fc_dp);
		/* 页和 lineoff 现在引用物理上下一个 tid */

		fc_linesleft = fc_lines - fc_lineoff + 1;
	}
	else if (fc_backward)
	{
		/* 不支持向后并行扫描 */
		Assert(fc_scan->rs_base.rs_parallel == NULL);

		if (!fc_scan->rs_inited)
		{
			/*
			 * 如果关系为空，则立即返回 null
			 */
			if (fc_scan->rs_nblocks == 0 || fc_scan->rs_numblocks == 0)
			{
				Assert(!BufferIsValid(fc_scan->rs_cbuf));
				fc_tuple->t_data = NULL;
				return;
			}

			/*
			 * 在向后扫描中禁用对 syncscan 逻辑的报告；
			 * 其他人同时做同样事情的可能性不大，
			 * 而我们更有可能只会干扰前进扫描器的行为。
			 */
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC;

			/*
			 * 从扫描的最后一页开始。确保我们考虑到 
			 * rs_numblocks 如果它被 heap_setscanlimits() 调整过。 
			 */
			if (fc_scan->rs_numblocks != InvalidBlockNumber)
				fc_page = (fc_scan->rs_startblock + fc_scan->rs_numblocks - 1) % fc_scan->rs_nblocks;
			else if (fc_scan->rs_startblock > 0)
				fc_page = fc_scan->rs_startblock - 1;
			else
				fc_page = fc_scan->rs_nblocks - 1;
			heapgetpage((TableScanDesc) fc_scan, fc_page);
		}
		else
		{
			/* 从先前返回的页面/元组继续 */
			fc_page = fc_scan->rs_cblock; /* 当前页面 */
		}

		LockBuffer(fc_scan->rs_cbuf, BUFFER_LOCK_SHARE);

		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lines = PageGetMaxOffsetNumber(fc_dp);

		if (!fc_scan->rs_inited)
		{
			fc_lineoff = fc_lines;	/* 最终 offnum */
			fc_scan->rs_inited = true;
		}
		else
		{
			/*
			 * 由于在使用非 MVCC 快照时，之前返回的元组可能已被 
			 * vacuumed，因此我们必须 
			 * 重新建立 lineoff <= PageGetMaxOffsetNumber(dp) 
			 * 不变性 
			 */
			fc_lineoff =			/* 之前的 offnum */
				Min(fc_lines,
					OffsetNumberPrev(ItemPointerGetOffsetNumber(&(fc_tuple->t_self))));
		}
		/* 页面和 lineoff 现在引用实际的前一个 tid */

		fc_linesleft = fc_lineoff;
	}
	else
	{
		/*
		 * ``无移动''扫描方向：重新获取之前的元组 
		 */
		if (!fc_scan->rs_inited)
		{
			Assert(!BufferIsValid(fc_scan->rs_cbuf));
			fc_tuple->t_data = NULL;
			return;
		}

		fc_page = ItemPointerGetBlockNumber(&(fc_tuple->t_self));
		if (fc_page != fc_scan->rs_cblock)
			heapgetpage((TableScanDesc) fc_scan, fc_page);

		/* 由于元组之前已被获取，因此这里不需要锁定页面 */
		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lineoff = ItemPointerGetOffsetNumber(&(fc_tuple->t_self));
		fc_lpp = PageGetItemId(fc_dp, fc_lineoff);
		Assert(ItemIdIsNormal(fc_lpp));

		fc_tuple->t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lpp);
		fc_tuple->t_len = ItemIdGetLength(fc_lpp);

		return;
	}

	/*
	 * 向前推进扫描，直到找到符合条件的元组或扫描完所有内容 
	 */
	fc_lpp = PageGetItemId(fc_dp, fc_lineoff);
	for (;;)
	{
		/*
		 * 只有在我们还有剩余行的时候才继续扫描页面。 
		 *
		 * 请注意，这可以保护我们不访问超出 
		 * PageGetMaxOffsetNumber() 的行指针；对前向扫描也是如此，当我们恢复 
		 * 表扫描时，以及当我们开始扫描新页面时。 
		 */
		while (fc_linesleft > 0)
		{
			if (ItemIdIsNormal(fc_lpp))
			{
				bool		fc_valid;

				fc_tuple->t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lpp);
				fc_tuple->t_len = ItemIdGetLength(fc_lpp);
				ItemPointerSet(&(fc_tuple->t_self), fc_page, fc_lineoff);

				/*
				 * 如果当前元组符合条件，返回它。
				 */
				fc_valid = HeapTupleSatisfiesVisibility(fc_tuple,
													 fc_snapshot,
													 fc_scan->rs_cbuf);

				HeapCheckForSerializableConflictOut(fc_valid, fc_scan->rs_base.rs_rd,
													fc_tuple, fc_scan->rs_cbuf,
													fc_snapshot);

				if (fc_valid && fc_key != NULL)
					HeapKeyTest(fc_tuple, RelationGetDescr(fc_scan->rs_base.rs_rd),
								fc_nkeys, fc_key, fc_valid);

				if (fc_valid)
				{
					LockBuffer(fc_scan->rs_cbuf, BUFFER_LOCK_UNLOCK);
					return;
				}
			}

			/*
			 * 否则移动到页面上的下一个项目 
			 */
			--fc_linesleft;
			if (fc_backward)
			{
				--fc_lpp;			/* 在该页面的 ItemId 数组中向后移动 */
				--fc_lineoff;
			}
			else
			{
				++fc_lpp;			/* 在该页面的 ItemId 数组中向前移动 */
				++fc_lineoff;
			}
		}

		/*
		 * 如果我们到达这里，意味着我们已经耗尽了该页面上的项目， 
		 * 是时候移动到下一个了。 
		 */
		LockBuffer(fc_scan->rs_cbuf, BUFFER_LOCK_UNLOCK);

		/*
		 * 前进到下一个/上一个页面并检测扫描结束 
		 */
		if (fc_backward)
		{
			fc_finished = (fc_page == fc_scan->rs_startblock) ||
				(fc_scan->rs_numblocks != InvalidBlockNumber ? --fc_scan->rs_numblocks == 0 : false);
			if (fc_page == 0)
				fc_page = fc_scan->rs_nblocks;
			fc_page--;
		}
		else if (fc_scan->rs_base.rs_parallel != NULL)
		{
			ParallelBlockTableScanDesc fc_pbscan =
			(ParallelBlockTableScanDesc) fc_scan->rs_base.rs_parallel;
			ParallelBlockTableScanWorker fc_pbscanwork =
			fc_scan->rs_parallelworkerdata;

			fc_page = table_block_parallelscan_nextpage(fc_scan->rs_base.rs_rd,
													 fc_pbscanwork, fc_pbscan);
			fc_finished = (fc_page == InvalidBlockNumber);
		}
		else
		{
			fc_page++;
			if (fc_page >= fc_scan->rs_nblocks)
				fc_page = 0;
			fc_finished = (fc_page == fc_scan->rs_startblock) ||
				(fc_scan->rs_numblocks != InvalidBlockNumber ? --fc_scan->rs_numblocks == 0 : false);

			/*
			 * 报告我们的新扫描位置以便于同步目的。 
			 * 但是在向后移动时不这样做。这只会 
			 * 混淆任何其他向前移动的扫描器。 
			 *
			 * 注意：我们在检查扫描结束之前执行此操作，以便 
			 * 位置提示的最终状态回到关系的起始位置。 
			 * 这不是严格必要的，但否则当您多次运行 
			 * 同一查询时，起始位置会在每次调用时稍微向 
			 * 后移动，这很令人困惑。 
			 * 不过，我们不会保证任何特定的排序。 
			 */
			if (fc_scan->rs_base.rs_flags & SO_ALLOW_SYNC)
				ss_report_location(fc_scan->rs_base.rs_rd, fc_page);
		}

		/*
		 * 如果我们已经耗尽了所有页面，则返回 NULL 
		 */
		if (fc_finished)
		{
			if (BufferIsValid(fc_scan->rs_cbuf))
				ReleaseBuffer(fc_scan->rs_cbuf);
			fc_scan->rs_cbuf = InvalidBuffer;
			fc_scan->rs_cblock = InvalidBlockNumber;
			fc_tuple->t_data = NULL;
			fc_scan->rs_inited = false;
			return;
		}

		heapgetpage((TableScanDesc) fc_scan, fc_page);

		LockBuffer(fc_scan->rs_cbuf, BUFFER_LOCK_SHARE);

		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lines = PageGetMaxOffsetNumber((Page) fc_dp);
		fc_linesleft = fc_lines;
		if (fc_backward)
		{
			fc_lineoff = fc_lines;
			fc_lpp = PageGetItemId(fc_dp, fc_lines);
		}
		else
		{
			fc_lineoff = FirstOffsetNumber;
			fc_lpp = PageGetItemId(fc_dp, FirstOffsetNumber);
		}
	}
}

/* ----------------
 *		heapgettup_pagemode - 以页为单位模式获取下一个堆元组
 *
 *		与 heapgettup 的 API 相同，但用于以页为单位的模式 
 *
 * 其内部逻辑与 heapgettup 也非常相似，但有一些 
 * 不同之处：我们不需要锁定缓冲区内容（这只需要 
 * 在 heapgetpage 中发生），并且我们只迭代 
 * rs_vistuples[] 中列出的元组，而不是页面上的所有元组。 
 * 请注意，lineindex 是从 0 开始的，而在 
 * heapgettup 中相应的循环变量 lineoff 是从 1 开始的。 
 * ----------------
 */
static void fc_heapgettup_pagemode(HeapScanDesc fc_scan,
					ScanDirection fc_dir,
					int fc_nkeys,
					ScanKey fc_key)
{
	HeapTuple	fc_tuple = &(fc_scan->rs_ctup);
	bool		fc_backward = ScanDirectionIsBackward(fc_dir);
	BlockNumber fc_page;
	bool		fc_finished;
	Page		fc_dp;
	int			fc_lines;
	int			fc_lineindex;
	OffsetNumber fc_lineoff;
	int			fc_linesleft;
	ItemId		fc_lpp;

	/*
	 * 根据扫描方向计算下一个起始 lineindex 
	 */
	if (ScanDirectionIsForward(fc_dir))
	{
		if (!fc_scan->rs_inited)
		{
			/*
			 * 如果关系为空，则立即返回 null
			 */
			if (fc_scan->rs_nblocks == 0 || fc_scan->rs_numblocks == 0)
			{
				Assert(!BufferIsValid(fc_scan->rs_cbuf));
				fc_tuple->t_data = NULL;
				return;
			}
			if (fc_scan->rs_base.rs_parallel != NULL)
			{
				ParallelBlockTableScanDesc fc_pbscan =
				(ParallelBlockTableScanDesc) fc_scan->rs_base.rs_parallel;
				ParallelBlockTableScanWorker fc_pbscanwork =
				fc_scan->rs_parallelworkerdata;

				table_block_parallelscan_startblock_init(fc_scan->rs_base.rs_rd,
														 fc_pbscanwork, fc_pbscan);

				fc_page = table_block_parallelscan_nextpage(fc_scan->rs_base.rs_rd,
														 fc_pbscanwork, fc_pbscan);

				/* 其他进程可能已经完成了扫描。 */
				if (fc_page == InvalidBlockNumber)
				{
					Assert(!BufferIsValid(fc_scan->rs_cbuf));
					fc_tuple->t_data = NULL;
					return;
				}
			}
			else
				fc_page = fc_scan->rs_startblock; /* 第一页 */
			heapgetpage((TableScanDesc) fc_scan, fc_page);
			fc_lineindex = 0;
			fc_scan->rs_inited = true;
		}
		else
		{
			/* 从先前返回的页面/元组继续 */
			fc_page = fc_scan->rs_cblock; /* 当前页面 */
			fc_lineindex = fc_scan->rs_cindex + 1;
		}

		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_scan->rs_base.rs_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lines = fc_scan->rs_ntuples;
		/* 页面和行索引现在引用下一个可见的 tid */

		fc_linesleft = fc_lines - fc_lineindex;
	}
	else if (fc_backward)
	{
		/* 不支持向后并行扫描 */
		Assert(fc_scan->rs_base.rs_parallel == NULL);

		if (!fc_scan->rs_inited)
		{
			/*
			 * 如果关系为空，则立即返回 null
			 */
			if (fc_scan->rs_nblocks == 0 || fc_scan->rs_numblocks == 0)
			{
				Assert(!BufferIsValid(fc_scan->rs_cbuf));
				fc_tuple->t_data = NULL;
				return;
			}

			/*
			 * 在向后扫描中禁用对 syncscan 逻辑的报告；
			 * 其他人同时做同样事情的可能性不大，
			 * 而我们更有可能只会干扰前进扫描器的行为。
			 */
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC;

			/*
			 * 从扫描的最后一页开始。确保我们考虑到 
			 * rs_numblocks 如果它被 heap_setscanlimits() 调整过。 
			 */
			if (fc_scan->rs_numblocks != InvalidBlockNumber)
				fc_page = (fc_scan->rs_startblock + fc_scan->rs_numblocks - 1) % fc_scan->rs_nblocks;
			else if (fc_scan->rs_startblock > 0)
				fc_page = fc_scan->rs_startblock - 1;
			else
				fc_page = fc_scan->rs_nblocks - 1;
			heapgetpage((TableScanDesc) fc_scan, fc_page);
		}
		else
		{
			/* 从先前返回的页面/元组继续 */
			fc_page = fc_scan->rs_cblock; /* 当前页面 */
		}

		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_scan->rs_base.rs_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lines = fc_scan->rs_ntuples;

		if (!fc_scan->rs_inited)
		{
			fc_lineindex = fc_lines - 1;
			fc_scan->rs_inited = true;
		}
		else
		{
			fc_lineindex = fc_scan->rs_cindex - 1;
		}
		/* 页面和行索引现在引用之前可见的 tid */

		fc_linesleft = fc_lineindex + 1;
	}
	else
	{
		/*
		 * ``无移动''扫描方向：重新获取之前的元组 
		 */
		if (!fc_scan->rs_inited)
		{
			Assert(!BufferIsValid(fc_scan->rs_cbuf));
			fc_tuple->t_data = NULL;
			return;
		}

		fc_page = ItemPointerGetBlockNumber(&(fc_tuple->t_self));
		if (fc_page != fc_scan->rs_cblock)
			heapgetpage((TableScanDesc) fc_scan, fc_page);

		/* 由于元组之前已被获取，因此这里不需要锁定页面 */
		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_scan->rs_base.rs_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lineoff = ItemPointerGetOffsetNumber(&(fc_tuple->t_self));
		fc_lpp = PageGetItemId(fc_dp, fc_lineoff);
		Assert(ItemIdIsNormal(fc_lpp));

		fc_tuple->t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lpp);
		fc_tuple->t_len = ItemIdGetLength(fc_lpp);

		/* 检查 rs_cindex 是否同步 */
		Assert(fc_scan->rs_cindex < fc_scan->rs_ntuples);
		Assert(fc_lineoff == fc_scan->rs_vistuples[fc_scan->rs_cindex]);

		return;
	}

	/*
	 * 向前推进扫描，直到找到符合条件的元组或扫描完所有内容 
	 */
	for (;;)
	{
		while (fc_linesleft > 0)
		{
			fc_lineoff = fc_scan->rs_vistuples[fc_lineindex];
			fc_lpp = PageGetItemId(fc_dp, fc_lineoff);
			Assert(ItemIdIsNormal(fc_lpp));

			fc_tuple->t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lpp);
			fc_tuple->t_len = ItemIdGetLength(fc_lpp);
			ItemPointerSet(&(fc_tuple->t_self), fc_page, fc_lineoff);

			/*
			 * 如果当前元组合格，则返回它。
			 */
			if (fc_key != NULL)
			{
				bool		fc_valid;

				HeapKeyTest(fc_tuple, RelationGetDescr(fc_scan->rs_base.rs_rd),
							fc_nkeys, fc_key, fc_valid);
				if (fc_valid)
				{
					fc_scan->rs_cindex = fc_lineindex;
					return;
				}
			}
			else
			{
				fc_scan->rs_cindex = fc_lineindex;
				return;
			}

			/*
			 * 否则移动到页面上的下一个项目 
			 */
			--fc_linesleft;
			if (fc_backward)
				--fc_lineindex;
			else
				++fc_lineindex;
		}

		/*
		 * 如果我们到达这里，意味着我们已经耗尽了该页面上的项目， 
		 * 是时候移动到下一个了。 
		 */
		if (fc_backward)
		{
			fc_finished = (fc_page == fc_scan->rs_startblock) ||
				(fc_scan->rs_numblocks != InvalidBlockNumber ? --fc_scan->rs_numblocks == 0 : false);
			if (fc_page == 0)
				fc_page = fc_scan->rs_nblocks;
			fc_page--;
		}
		else if (fc_scan->rs_base.rs_parallel != NULL)
		{
			ParallelBlockTableScanDesc fc_pbscan =
			(ParallelBlockTableScanDesc) fc_scan->rs_base.rs_parallel;
			ParallelBlockTableScanWorker fc_pbscanwork =
			fc_scan->rs_parallelworkerdata;

			fc_page = table_block_parallelscan_nextpage(fc_scan->rs_base.rs_rd,
													 fc_pbscanwork, fc_pbscan);
			fc_finished = (fc_page == InvalidBlockNumber);
		}
		else
		{
			fc_page++;
			if (fc_page >= fc_scan->rs_nblocks)
				fc_page = 0;
			fc_finished = (fc_page == fc_scan->rs_startblock) ||
				(fc_scan->rs_numblocks != InvalidBlockNumber ? --fc_scan->rs_numblocks == 0 : false);

			/*
			 * 报告我们的新扫描位置以便于同步目的。 
			 * 但是在向后移动时不这样做。这只会 
			 * 混淆任何其他向前移动的扫描器。 
			 *
			 * 注意：我们在检查扫描结束之前执行此操作，以便 
			 * 位置提示的最终状态回到关系的起始位置。 
			 * 这不是严格必要的，但否则当您多次运行 
			 * 同一查询时，起始位置会在每次调用时稍微向 
			 * 后移动，这很令人困惑。 
			 * 不过，我们不会保证任何特定的排序。 
			 */
			if (fc_scan->rs_base.rs_flags & SO_ALLOW_SYNC)
				ss_report_location(fc_scan->rs_base.rs_rd, fc_page);
		}

		/*
		 * 如果我们已经耗尽了所有页面，则返回 NULL 
		 */
		if (fc_finished)
		{
			if (BufferIsValid(fc_scan->rs_cbuf))
				ReleaseBuffer(fc_scan->rs_cbuf);
			fc_scan->rs_cbuf = InvalidBuffer;
			fc_scan->rs_cblock = InvalidBlockNumber;
			fc_tuple->t_data = NULL;
			fc_scan->rs_inited = false;
			return;
		}

		heapgetpage((TableScanDesc) fc_scan, fc_page);

		fc_dp = BufferGetPage(fc_scan->rs_cbuf);
		TestForOldSnapshot(fc_scan->rs_base.rs_snapshot, fc_scan->rs_base.rs_rd, fc_dp);
		fc_lines = fc_scan->rs_ntuples;
		fc_linesleft = fc_lines;
		if (fc_backward)
			fc_lineindex = fc_lines - 1;
		else
			fc_lineindex = 0;
	}
}


/* ----------------------------------------------------------------
 *					 堆访问方法接口
 * ----------------------------------------------------------------
 */


TableScanDesc heap_beginscan(Relation fc_relation, Snapshot fc_snapshot,
			   int fc_nkeys, ScanKey fc_key,
			   ParallelTableScanDesc fc_parallel_scan,
			   uint32 fc_flags)
{
	HeapScanDesc fc_scan;

	/*
	 * 在扫描关系时增加关系引用计数
	 *
	 * 这只是为了确保 relcache 条目在扫描过程中不会消失。
	 * 调用者应该持有该关系打开，因此在所有正常情况下
	 * 这都是多余的...
	 */
	RelationIncrementReferenceCount(fc_relation);

	/*
	 * 分配和初始化扫描描述符
	 */
	fc_scan = (HeapScanDesc) palloc(sizeof(HeapScanDescData));

	fc_scan->rs_base.rs_rd = fc_relation;
	fc_scan->rs_base.rs_snapshot = fc_snapshot;
	fc_scan->rs_base.rs_nkeys = fc_nkeys;
	fc_scan->rs_base.rs_flags = fc_flags;
	fc_scan->rs_base.rs_parallel = fc_parallel_scan;
	fc_scan->rs_strategy = NULL;	/* 在 initscan 中设置 */

	/*
	 * 如果不是 MVCC 安全快照，则禁用逐页模式。
	*/
	if (!(fc_snapshot && IsMVCCSnapshot(fc_snapshot)))
		fc_scan->rs_base.rs_flags &= ~SO_ALLOW_PAGEMODE;

	/*
	 * 对于可序列化事务中的序列扫描和样本扫描，获取整个关系上的谓词锁。这不仅需要锁定所有匹配的元组，还要与表中的新插入冲突。在索引扫描中，我们对覆盖扫描条件中指定范围的索引页面进行页面锁定，但在堆扫描中没有更细粒度的锁定。位图扫描则是另一回事，在那里我们已经扫描了索引并锁定了覆盖谓词的索引页面。但在这种情况下，我们仍然需要锁定任何匹配的堆元组。对于样本扫描，我们可以将锁定优化到至少页面级别的粒度，但我们需要为此添加每个元组的锁定。
	 */
	if (fc_scan->rs_base.rs_flags & (SO_TYPE_SEQSCAN | SO_TYPE_SAMPLESCAN))
	{
		/*
		 * 确保缺失的快照被可靠地注意到，即使隔离模式意味着不执行谓词锁定（因此在这里不使用快照）。
		 */
		Assert(fc_snapshot);
		PredicateLockRelation(fc_relation, fc_snapshot);
	}

	/* 我们只需要设置一次 */
	fc_scan->rs_ctup.t_tableOid = RelationGetRelid(fc_relation);

	/*
	 * 分配内存以跟踪并行工作线程在做并行扫描时的页面分配。
	 */
	if (fc_parallel_scan != NULL)
		fc_scan->rs_parallelworkerdata = palloc(sizeof(ParallelBlockTableScanWorkerData));
	else
		fc_scan->rs_parallelworkerdata = NULL;

	/*
	 * 我们在这里而不是在 initscan() 中执行此操作，因为 heap_rescan 也调用 initscan()，我们不想再次分配内存。
	 */
	if (fc_nkeys > 0)
		fc_scan->rs_base.rs_key = (ScanKey) palloc(sizeof(ScanKeyData) * fc_nkeys);
	else
		fc_scan->rs_base.rs_key = NULL;

	fc_initscan(fc_scan, fc_key, false);

	return (TableScanDesc) fc_scan;
}

void heap_rescan(TableScanDesc fc_sscan, ScanKey fc_key, bool fc_set_params,
			bool fc_allow_strat, bool fc_allow_sync, bool fc_allow_pagemode)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;

	if (fc_set_params)
	{
		if (fc_allow_strat)
			fc_scan->rs_base.rs_flags |= SO_ALLOW_STRAT;
		else
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_STRAT;

		if (fc_allow_sync)
			fc_scan->rs_base.rs_flags |= SO_ALLOW_SYNC;
		else
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC;

		if (fc_allow_pagemode && fc_scan->rs_base.rs_snapshot &&
			IsMVCCSnapshot(fc_scan->rs_base.rs_snapshot))
			fc_scan->rs_base.rs_flags |= SO_ALLOW_PAGEMODE;
		else
			fc_scan->rs_base.rs_flags &= ~SO_ALLOW_PAGEMODE;
	}

	/*
	 * 解锁扫描缓冲区
	 */
	if (BufferIsValid(fc_scan->rs_cbuf))
		ReleaseBuffer(fc_scan->rs_cbuf);

	/*
	 * 重新初始化扫描描述符
	 */
	fc_initscan(fc_scan, fc_key, true);
}

void heap_endscan(TableScanDesc fc_sscan)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;

	/* 注意：无需锁定操作 */

	/*
	 * 解锁扫描缓冲区
	 */
	if (BufferIsValid(fc_scan->rs_cbuf))
		ReleaseBuffer(fc_scan->rs_cbuf);

	/*
	 * 递减关系引用计数并释放扫描描述符存储
	 */
	RelationDecrementReferenceCount(fc_scan->rs_base.rs_rd);

	if (fc_scan->rs_base.rs_key)
		pfree(fc_scan->rs_base.rs_key);

	if (fc_scan->rs_strategy != NULL)
		FreeAccessStrategy(fc_scan->rs_strategy);

	if (fc_scan->rs_parallelworkerdata != NULL)
		pfree(fc_scan->rs_parallelworkerdata);

	if (fc_scan->rs_base.rs_flags & SO_TEMP_SNAPSHOT)
		UnregisterSnapshot(fc_scan->rs_base.rs_snapshot);

	pfree(fc_scan);
}

HeapTuple heap_getnext(TableScanDesc fc_sscan, ScanDirection fc_direction)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;

	/*
	 * 这仍然被广泛直接使用，未通过表 AM，因此添加安全检查。可能在后期我们应该将其降级为断言。检查 AM 例程而不是 AM oid 的原因是，这允许编写回归测试来创建另一个重用堆处理程序的 AM。
	 */
	if (unlikely(fc_sscan->rs_rd->rd_tableam != GetHeapamTableAmRoutine()))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg_internal("only heap AM is supported")));

	/*
	 * 我们不期望对目录或常规表直接调用 heap_getnext，带有有效的 CheckXidAlive。有关这些变量声明的详细注释，请参阅 xact.c。通常，我们在 tableam 级别的 API 中有这样的检查，但由于从许多地方调用此函数，所以我们需要在这里确保。
	 */
	if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan))
		elog(ERROR, "unexpected heap_getnext call during logical decoding");

	/* 注意：无需锁定操作 */

	if (fc_scan->rs_base.rs_flags & SO_ALLOW_PAGEMODE)
		fc_heapgettup_pagemode(fc_scan, fc_direction,
							fc_scan->rs_base.rs_nkeys, fc_scan->rs_base.rs_key);
	else
		fc_heapgettup(fc_scan, fc_direction,
				   fc_scan->rs_base.rs_nkeys, fc_scan->rs_base.rs_key);

	if (fc_scan->rs_ctup.t_data == NULL)
		return NULL;

	/*
	 * 如果我们到这里，意味着我们有一个新的当前扫描元组，因此指向适当的返回缓冲区并返回元组。
	 */

	pgstat_count_heap_getnext(fc_scan->rs_base.rs_rd);

	return &fc_scan->rs_ctup;
}

bool heap_getnextslot(TableScanDesc fc_sscan, ScanDirection fc_direction, TupleTableSlot *fc_slot)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;

	/* 注意：无需锁定操作 */

	if (fc_sscan->rs_flags & SO_ALLOW_PAGEMODE)
		fc_heapgettup_pagemode(fc_scan, fc_direction, fc_sscan->rs_nkeys, fc_sscan->rs_key);
	else
		fc_heapgettup(fc_scan, fc_direction, fc_sscan->rs_nkeys, fc_sscan->rs_key);

	if (fc_scan->rs_ctup.t_data == NULL)
	{
		ExecClearTuple(fc_slot);
		return false;
	}

	/*
	 * 如果我们到这里，意味着我们有一个新的当前扫描元组，因此指向适当的返回缓冲区并返回元组。
	 */

	pgstat_count_heap_getnext(fc_scan->rs_base.rs_rd);

	ExecStoreBufferHeapTuple(&fc_scan->rs_ctup, fc_slot,
							 fc_scan->rs_cbuf);
	return true;
}

void heap_set_tidrange(TableScanDesc fc_sscan, ItemPointer fc_mintid,
				  ItemPointer fc_maxtid)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;
	BlockNumber fc_startBlk;
	BlockNumber fc_numBlks;
	ItemPointerData fc_highestItem;
	ItemPointerData fc_lowestItem;

	/*
	 * 对于没有任何页面的关系，我们可以简单地将 TID 范围保持为未设置。将没有要扫描的元组，因此也没有在给定 TID 范围之外的元组。
	 */
	if (fc_scan->rs_nblocks == 0)
		return;

	/*
	 * 设置一些 ItemPointers，指向堆中的第一个和最后一个可能的元组。
	 */
	ItemPointerSet(&fc_highestItem, fc_scan->rs_nblocks - 1, MaxOffsetNumber);
	ItemPointerSet(&fc_lowestItem, 0, FirstOffsetNumber);

	/*
	 * 如果给定的最大 TID 低于关系中可能的最高 TID，则将范围限制为该值，否则我们扫描到关系的末尾。
	 */
	if (ItemPointerCompare(fc_maxtid, &fc_highestItem) < 0)
		ItemPointerCopy(fc_maxtid, &fc_highestItem);

	/*
	 * 如果给定的最小 TID 高于关系中可能的最低 TID，则仅限制范围以扫描该值以上的 TID。
	 */
	if (ItemPointerCompare(fc_mintid, &fc_lowestItem) > 0)
		ItemPointerCopy(fc_mintid, &fc_lowestItem);

	/*
	 * 检查是否有空范围，并防止下面的 numBlks 计算出现负结果。
	 */
	if (ItemPointerCompare(&fc_highestItem, &fc_lowestItem) < 0)
	{
		/* 设置一个空的块范围以进行扫描 */
		heap_setscanlimits(fc_sscan, 0, 0);
		return;
	}

	/*
	 * 计算我们必须扫描的第一个块和块的数量。我们可以更激进一些，进行更多验证，以尝试通过检查 lowerItem 是否有超过 MaxOffsetNumber 的偏移量，进一步缩小要扫描的块的范围。在这种情况下，我们可以将 startBlk 向前推进一个。类似地，如果 highestItem 的偏移量为 0，我们可以少扫描一个块。然而，目前看来，这样的优化似乎不值得费心。
	 */
	fc_startBlk = ItemPointerGetBlockNumberNoCheck(&fc_lowestItem);

	fc_numBlks = ItemPointerGetBlockNumberNoCheck(&fc_highestItem) -
		ItemPointerGetBlockNumberNoCheck(&fc_lowestItem) + 1;

	/* 设置起始块和要扫描的块数 */
	heap_setscanlimits(fc_sscan, fc_startBlk, fc_numBlks);

	/* 最后，在 sscan 中设置 TID 范围 */
	ItemPointerCopy(&fc_lowestItem, &fc_sscan->rs_mintid);
	ItemPointerCopy(&fc_highestItem, &fc_sscan->rs_maxtid);
}

bool heap_getnextslot_tidrange(TableScanDesc fc_sscan, ScanDirection fc_direction,
						  TupleTableSlot *fc_slot)
{
	HeapScanDesc fc_scan = (HeapScanDesc) fc_sscan;
	ItemPointer fc_mintid = &fc_sscan->rs_mintid;
	ItemPointer fc_maxtid = &fc_sscan->rs_maxtid;

	/* 注意：无需锁定操作 */
	for (;;)
	{
		if (fc_sscan->rs_flags & SO_ALLOW_PAGEMODE)
			fc_heapgettup_pagemode(fc_scan, fc_direction, fc_sscan->rs_nkeys, fc_sscan->rs_key);
		else
			fc_heapgettup(fc_scan, fc_direction, fc_sscan->rs_nkeys, fc_sscan->rs_key);

		if (fc_scan->rs_ctup.t_data == NULL)
		{
			ExecClearTuple(fc_slot);
			return false;
		}

		/*
		 * heap_set_tidrange 将使用 heap_setscanlimits 将我们要扫描的页面范围限制为仅包含可能包含我们正在扫描的 TID 范围的页面。在这里，我们必须过滤掉这些页面中超出该范围的任何元组。
		 */
		if (ItemPointerCompare(&fc_scan->rs_ctup.t_self, fc_mintid) < 0)
		{
			ExecClearTuple(fc_slot);

			/*
			 * 在向后扫描时，TID 将按降序排列。在此方向上的未来元组将更多地偏低，因此我们可以仅返回 false 以表明将没有更多的元组。
			 */
			if (ScanDirectionIsBackward(fc_direction))
				return false;

			continue;
		}

		/*
		 * 对于最后一页，我们必须过滤掉大于 maxtid 的 TID。
		 */
		if (ItemPointerCompare(&fc_scan->rs_ctup.t_self, fc_maxtid) > 0)
		{
			ExecClearTuple(fc_slot);

			/*
			 * 在向前扫描时，TID 将按升序排列。在此方向上的未来元组将更多地偏高，因此我们可以仅返回 false 以表明将没有更多的元组。
			 */
			if (ScanDirectionIsForward(fc_direction))
				return false;
			continue;
		}

		break;
	}

	/*
	 * 如果我们到这里，意味着我们有一个新的当前扫描元组，因此指向适当的返回缓冲区并返回元组。
	 */
	pgstat_count_heap_getnext(fc_scan->rs_base.rs_rd);

	ExecStoreBufferHeapTuple(&fc_scan->rs_ctup, fc_slot, fc_scan->rs_cbuf);
	return true;
}

/*
 *	heap_fetch		- 获取具有给定TID的元组
 *
 * 在进入时，tuple->t_self是要获取的TID。我们锁定持有
 * 元组的缓冲区，填充*tuple的其余字段，并检查元组
 * 与指定快照的符合性。
 *
 * 如果成功（找到元组并通过快照时间质量检查），则设置*userbuf
 * 为持有元组的缓冲区，并返回true。调用方
 * 在完成对元组的操作后必须解锁缓冲区。
 *
 * 如果未找到元组（即，项号引用了一个已删除的槽），
 * 则tuple->t_data被设置为NULL，*userbuf被设置为InvalidBuffer，
 * 并返回false。
 *
 * 如果找到了元组但未通过时间质量检查，则行为
 * 取决于keep_buf参数。如果keep_buf为false，则结果
 * 与未找到元组的情况相同。如果keep_buf为true，
 * 则tuple->t_data和*userbuf将被作为成功的情况返回，
 * 并且调用方必须解锁缓冲区；但返回false。
 *
 * heap_fetch不遵循HOT链：只会获取请求的精确TID。
 *
 * 在无效块号时我们调用ereport()但在无效项号时返回false的行为有点不一致。
 * 但是有几个原因。一个是调用方可以相对轻松地检查块号的
 * 有效性，但无法在不读取页面的情况下检查项号。
 * 另一个是当我们跟随t_ctid链接时，可以合理地
 * 确信页面号是有效的（因为VACUUM不应该在未先删除
 * 引用元组的情况下截断目标页面），但项号可能确实无效。
 */
bool heap_fetch(Relation fc_relation,
		   Snapshot fc_snapshot,
		   HeapTuple fc_tuple,
		   Buffer *fc_userbuf,
		   bool fc_keep_buf)
{
	ItemPointer fc_tid = &(fc_tuple->t_self);
	ItemId		fc_lp;
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	bool		fc_valid;

	/*
	 * 获取并锁定关系的适当页面。
	 */
	fc_buffer = ReadBuffer(fc_relation, ItemPointerGetBlockNumber(fc_tid));

	/*
	 * 需要共享锁定缓冲区以检查元组提交状态。
	 */
	LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);
	fc_page = BufferGetPage(fc_buffer);
	TestForOldSnapshot(fc_snapshot, fc_relation, fc_page);

	/*
	 * 我们最好在VACUUM情况下检查越界的offnum，因为TID已获取。
	 */
	fc_offnum = ItemPointerGetOffsetNumber(fc_tid);
	if (fc_offnum < FirstOffsetNumber || fc_offnum > PageGetMaxOffsetNumber(fc_page))
	{
		LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
		ReleaseBuffer(fc_buffer);
		*fc_userbuf = InvalidBuffer;
		fc_tuple->t_data = NULL;
		return false;
	}

	/*
	 * 获取与请求的tid对应的项行指针
	 */
	fc_lp = PageGetItemId(fc_page, fc_offnum);

	/*
	 * 必须检查已删除的元组。
	 */
	if (!ItemIdIsNormal(fc_lp))
	{
		LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
		ReleaseBuffer(fc_buffer);
		*fc_userbuf = InvalidBuffer;
		fc_tuple->t_data = NULL;
		return false;
	}

	/*
	 * 填充*tuple字段
	 */
	fc_tuple->t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);
	fc_tuple->t_len = ItemIdGetLength(fc_lp);
	fc_tuple->t_tableOid = RelationGetRelid(fc_relation);

	/*
	 * 检查元组的可见性，然后释放锁定
	 */
	fc_valid = HeapTupleSatisfiesVisibility(fc_tuple, fc_snapshot, fc_buffer);

	if (fc_valid)
		PredicateLockTID(fc_relation, &(fc_tuple->t_self), fc_snapshot,
						 HeapTupleHeaderGetXmin(fc_tuple->t_data));

	HeapCheckForSerializableConflictOut(fc_valid, fc_relation, fc_tuple, fc_buffer, fc_snapshot);

	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

	if (fc_valid)
	{
		/*
		 * 所有检查通过，因此返回有效的元组。调用方现在
		 * 负责释放缓冲区。
		 */
		*fc_userbuf = fc_buffer;

		return true;
	}

	/* 元组未通过时间质量检查，但可能调用方仍想查看。 */
	if (fc_keep_buf)
		*fc_userbuf = fc_buffer;
	else
	{
		ReleaseBuffer(fc_buffer);
		*fc_userbuf = InvalidBuffer;
		fc_tuple->t_data = NULL;
	}

	return false;
}

/*
 *	heap_hot_search_buffer	- 在 HOT 链中搜索满足快照的元组
 *
 * 进入时，*tid 是元组的 TID（可以是简单元组或 HOT 链的根），
 * buffer 是持有该元组的缓冲区。我们搜索第一个满足给定快照的链成员。
 * 如果找到，则更新 *tid 以引用该元组的偏移量，并返回 true。
 * 如果没有匹配，则返回 false 而不修改 *tid。
 *
 * heapTuple 是调用者提供的缓冲区。当找到匹配时，我们在此返回
 * 元组，并更新 *tid。如果未找到匹配，返回时该缓冲区的内容是不确定的。
 *
 * 如果 all_dead 不为 NULL，我们检查不可见元组以查看它们是否
 * 全局死亡；如果 HOT 链的所有成员都是可清理的，则 *all_dead 设置为 true，如不是则为 false。
 *
 * 与 heap_fetch 不同，调用者必须已经在缓冲区上加锁（至少是共享锁）；
 * 在退出时仍然保持被锁定/钉住状态。
 */
bool heap_hot_search_buffer(ItemPointer fc_tid, Relation fc_relation, Buffer fc_buffer,
					   Snapshot fc_snapshot, HeapTuple fc_heapTuple,
					   bool *fc_all_dead, bool fc_first_call)
{
	Page		fc_dp = (Page) BufferGetPage(fc_buffer);
	TransactionId fc_prev_xmax = InvalidTransactionId;
	BlockNumber fc_blkno;
	OffsetNumber fc_offnum;
	bool		fc_at_chain_start;
	bool		fc_valid;
	bool		fc_skip;
	GlobalVisState *fc_vistest = NULL;

	/* 如果这不是第一次调用，则上一次调用返回了一个（有效！）元组 */
	if (fc_all_dead)
		*fc_all_dead = fc_first_call;

	fc_blkno = ItemPointerGetBlockNumber(fc_tid);
	fc_offnum = ItemPointerGetOffsetNumber(fc_tid);
	fc_at_chain_start = fc_first_call;
	fc_skip = !fc_first_call;

	/* XXX: 我们应该断言已经推送或注册了一个快照 */
	Assert(TransactionIdIsValid(RecentXmin));
	Assert(BufferGetBlockNumber(fc_buffer) == fc_blkno);

	/* 扫描可能的多个 HOT 链成员 */
	for (;;)
	{
		ItemId		fc_lp;

		/* 检查伪 TID */
		if (fc_offnum < FirstOffsetNumber || fc_offnum > PageGetMaxOffsetNumber(fc_dp))
			break;

		fc_lp = PageGetItemId(fc_dp, fc_offnum);

		/* 检查未使用、已死亡或重定向的项 */
		if (!ItemIdIsNormal(fc_lp))
		{
			/* 我们只应在链开始看到重定向 */
			if (ItemIdIsRedirected(fc_lp) && fc_at_chain_start)
			{
				/* 跟踪重定向 */
				fc_offnum = ItemIdGetRedirect(fc_lp);
				fc_at_chain_start = false;
				continue;
			}
			/* 否则必须是链的末尾 */
			break;
		}

		/*
		 * 更新 heapTuple 以指向我们当前正在调查的 HOT 链的元素。
		 * 正确设置 t_self 是重要的，因为 SSI 检查和用于历史
		 * MVCC 快照的 *Satisfies 例程需要正确的 tid 来决定可见性。
		 */
		fc_heapTuple->t_data = (HeapTupleHeader) PageGetItem(fc_dp, fc_lp);
		fc_heapTuple->t_len = ItemIdGetLength(fc_lp);
		fc_heapTuple->t_tableOid = RelationGetRelid(fc_relation);
		ItemPointerSet(&fc_heapTuple->t_self, fc_blkno, fc_offnum);

		/*
		 * 在链的开始位置不应看到 HEAP_ONLY 元组。
		 */
		if (fc_at_chain_start && HeapTupleIsHeapOnly(fc_heapTuple))
			break;

		/*
		 * xmin 应该与前一个 xmax 值匹配，否则链会断裂。
		 */
		if (TransactionIdIsValid(fc_prev_xmax) &&
			!TransactionIdEquals(fc_prev_xmax,
								 HeapTupleHeaderGetXmin(fc_heapTuple->t_data)))
			break;

		/*
		 * 当 first_call 为 true（因此 skip 初始为 false）时，我们
		 * 将返回我们找到的第一个元组。但在后续遍历中，heapTuple
		 * 最初将指向我们上次返回的元组。
		 * 再次返回它是不正确的（并且会导致无限循环），所以
		 * 我们跳过它，返回找到的下一个匹配项。
		 */
		if (!fc_skip)
		{
			/* 如果它对于快照是可见的，我们必须返回它 */
			fc_valid = HeapTupleSatisfiesVisibility(fc_heapTuple, fc_snapshot, fc_buffer);
			HeapCheckForSerializableConflictOut(fc_valid, fc_relation, fc_heapTuple,
												fc_buffer, fc_snapshot);

			if (fc_valid)
			{
				ItemPointerSetOffsetNumber(fc_tid, fc_offnum);
				PredicateLockTID(fc_relation, &fc_heapTuple->t_self, fc_snapshot,
								 HeapTupleHeaderGetXmin(fc_heapTuple->t_data));
				if (fc_all_dead)
					*fc_all_dead = false;
				return true;
			}
		}
		fc_skip = false;

		/*
		 * 如果我们看不见它，可能其他人也看不见。
		 * 根据调用者的请求，检查是否所有链成员对所有
		 * 事务都是死亡的。
		 *
		 * 注意：如果您在这里更改了“死亡”的标准，请
		 * 修复规划器的 get_actual_variable_range() 函数以匹配。
		 */
		if (fc_all_dead && *fc_all_dead)
		{
			if (!fc_vistest)
				fc_vistest = GlobalVisTestFor(fc_relation);

			if (!HeapTupleIsSurelyDead(fc_heapTuple, fc_vistest))
				*fc_all_dead = false;
		}

		/*
		 * 检查 HOT 链是否在此元组之后继续；如果是，则获取下一个 offnum 并循环。
		 */
		if (HeapTupleIsHotUpdated(fc_heapTuple))
		{
			Assert(ItemPointerGetBlockNumber(&fc_heapTuple->t_data->t_ctid) ==
				   fc_blkno);
			fc_offnum = ItemPointerGetOffsetNumber(&fc_heapTuple->t_data->t_ctid);
			fc_at_chain_start = false;
			fc_prev_xmax = HeapTupleHeaderGetUpdateXid(fc_heapTuple->t_data);
		}
		else
			break;				/* 链的结束 */
	}

	return false;
}

/*
 *	heap_get_latest_tid - 获取指定元组的最新 tid
 *
 * 实际上，这获取的是根据扫描的快照可见的最新版本。使用 SnapshotDirty 创建扫描以获取最新的，
 * 可能是未提交的版本。
 *
 * *tid 是输入和输出参数：它会被更新以显示该行的最新版本。请注意，如果没有
 * 版本通过快照测试，它是不会被更改的。
 */
void heap_get_latest_tid(TableScanDesc fc_sscan,
					ItemPointer fc_tid)
{
	Relation	fc_relation = fc_sscan->rs_rd;
	Snapshot	fc_snapshot = fc_sscan->rs_snapshot;
	ItemPointerData fc_ctid;
	TransactionId fc_priorXmax;

	/*
	 * table_tuple_get_latest_tid() 验证传入的 tid 是有效的。
	 * 假设 t_ctid 链接是有效的，表中不应该有无效的链接。
	 */
	Assert(ItemPointerIsValid(fc_tid));

	/*
	 * 循环追踪 t_ctid 链接。在循环开始时，ctid 是我们
	 * 需要检查的元组，而 *tid 是我们将返回的 TID，如果 ctid 被证明是无效的。
	 *
	 * 请注意，我们会循环直到到达 t_ctid 链接的末尾。
	 * 根据所传递的快照，最多可能只有一个可见
	 * 行的版本，但我们不试图对此进行优化。
	 */
	fc_ctid = *fc_tid;
	fc_priorXmax = InvalidTransactionId;	/* 无法检查第一个 XMIN */
	for (;;)
	{
		Buffer		fc_buffer;
		Page		fc_page;
		OffsetNumber fc_offnum;
		ItemId		fc_lp;
		HeapTupleData fc_tp;
		bool		fc_valid;

		/*
		 * 读取、固定并锁定页面。
		 */
		fc_buffer = ReadBuffer(fc_relation, ItemPointerGetBlockNumber(&fc_ctid));
		LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);
		fc_page = BufferGetPage(fc_buffer);
		TestForOldSnapshot(fc_snapshot, fc_relation, fc_page);

		/*
		 * 检查虚假项目编号。由于在跟踪 t_ctid 链接时可能会发生这种情况，因此
		 * 这不是作为错误条件处理。我们只是假设之前的 tid 是好的，并返回它而不做更改。
		 */
		fc_offnum = ItemPointerGetOffsetNumber(&fc_ctid);
		if (fc_offnum < FirstOffsetNumber || fc_offnum > PageGetMaxOffsetNumber(fc_page))
		{
			UnlockReleaseBuffer(fc_buffer);
			break;
		}
		fc_lp = PageGetItemId(fc_page, fc_offnum);
		if (!ItemIdIsNormal(fc_lp))
		{
			UnlockReleaseBuffer(fc_buffer);
			break;
		}

		/* 可以访问元组 */
		fc_tp.t_self = fc_ctid;
		fc_tp.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);
		fc_tp.t_len = ItemIdGetLength(fc_lp);
		fc_tp.t_tableOid = RelationGetRelid(fc_relation);

		/*
		 * 在跟踪 t_ctid 链接后，我们可能到达一个无关的
		 * 元组。检查 XMIN 是否匹配。
		 */
		if (TransactionIdIsValid(fc_priorXmax) &&
			!TransactionIdEquals(fc_priorXmax, HeapTupleHeaderGetXmin(fc_tp.t_data)))
		{
			UnlockReleaseBuffer(fc_buffer);
			break;
		}

		/*
		 * 检查元组可见性；如果可见，则将其设置为新的结果
		 * 候选项。
		 */
		fc_valid = HeapTupleSatisfiesVisibility(&fc_tp, fc_snapshot, fc_buffer);
		HeapCheckForSerializableConflictOut(fc_valid, fc_relation, &fc_tp, fc_buffer, fc_snapshot);
		if (fc_valid)
			*fc_tid = fc_ctid;

		/*
		 * 如果有有效的 t_ctid 链接，则跟踪它，否则我们结束。
		 */
		if ((fc_tp.t_data->t_infomask & HEAP_XMAX_INVALID) ||
			HeapTupleHeaderIsOnlyLocked(fc_tp.t_data) ||
			HeapTupleHeaderIndicatesMovedPartitions(fc_tp.t_data) ||
			ItemPointerEquals(&fc_tp.t_self, &fc_tp.t_data->t_ctid))
		{
			UnlockReleaseBuffer(fc_buffer);
			break;
		}

		fc_ctid = fc_tp.t_data->t_ctid;
		fc_priorXmax = HeapTupleHeaderGetUpdateXid(fc_tp.t_data);
		UnlockReleaseBuffer(fc_buffer);
	}							/* 循环结束 */
}


/*
 * UpdateXmaxHintBits - 在 xmax 事务结束后更新元组提示位
 *
 * 在我们等待 XMAX 事务终止后调用此方法。
 * 如果事务中止，我们保证退出时将设置 XMAX_INVALID 提示位。如果事务提交，
 * 如果可能，我们设置 XMAX_COMMITTED 提示位 —— 但请注意，如果事务是异步提交的，
 * 那么这可能尚不可行。
 *
 * 请注意，如果事务仅仅是一个锁定者，即使它提交，我们也会设置 HEAP_XMAX_INVALID。
 *
 * 因此调用者应该只关注 XMAX_INVALID。
 *
 * 请注意，如果元组的 xmax 是 multixact，则不允许这样做。
 */
static void fc_UpdateXmaxHintBits(HeapTupleHeader fc_tuple, Buffer fc_buffer, TransactionId fc_xid)
{
	Assert(TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tuple), fc_xid));
	Assert(!(fc_tuple->t_infomask & HEAP_XMAX_IS_MULTI));

	if (!(fc_tuple->t_infomask & (HEAP_XMAX_COMMITTED | HEAP_XMAX_INVALID)))
	{
		if (!HEAP_XMAX_IS_LOCKED_ONLY(fc_tuple->t_infomask) &&
			TransactionIdDidCommit(fc_xid))
			HeapTupleSetHintBits(fc_tuple, fc_buffer, HEAP_XMAX_COMMITTED,
								 fc_xid);
		else
			HeapTupleSetHintBits(fc_tuple, fc_buffer, HEAP_XMAX_INVALID,
								 InvalidTransactionId);
	}
}


/*
 * GetBulkInsertState - 为批量插入准备状态对象
 */
BulkInsertState
GetBulkInsertState(void)
{
	BulkInsertState fc_bistate;

	fc_bistate = (BulkInsertState) palloc(sizeof(BulkInsertStateData));
	fc_bistate->strategy = GetAccessStrategy(BAS_BULKWRITE);
	fc_bistate->current_buf = InvalidBuffer;
	return fc_bistate;
}

/*
 * FreeBulkInsertState - 在完成批量插入后进行清理
 */
void FreeBulkInsertState(BulkInsertState fc_bistate)
{
	if (fc_bistate->current_buf != InvalidBuffer)
		ReleaseBuffer(fc_bistate->current_buf);
	FreeAccessStrategy(fc_bistate->strategy);
	pfree(fc_bistate);
}

/*
 * ReleaseBulkInsertStatePin - 释放当前在 bistate 中保持的缓冲区
 */
void ReleaseBulkInsertStatePin(BulkInsertState fc_bistate)
{
	if (fc_bistate->current_buf != InvalidBuffer)
		ReleaseBuffer(fc_bistate->current_buf);
	fc_bistate->current_buf = InvalidBuffer;
}


/*
 *	heap_insert		- 将元组插入堆中
 *
 * 新元组被标记为当前事务ID和指定的命令ID。
 *
 * 请参见table_tuple_insert关于大多数输入标志的评论，不同之处在于
 * 这个例程直接接受一个元组而不是一个槽。
 *
 * 所有TABLE_INSERT_选项都有相应的HEAP_INSERT_选项，
 * 此外还有HEAP_INSERT_SPECULATIVE，用于实现table_tuple_insert_speculative()。
 *
 * 返回时，*tup的标题字段已更新以匹配存储的元组；
 * 特别是tup->t_self接收到元组存储位置的实际TID。
 * 但请注意，元组数据中任何字段的烤制（toasting）不会反映到*tup中。
 */
void heap_insert(Relation fc_relation, HeapTuple fc_tup, CommandId fc_cid,
			int fc_options, BulkInsertState fc_bistate)
{
	TransactionId fc_xid = GetCurrentTransactionId();
	HeapTuple	fc_heaptup;
	Buffer		fc_buffer;
	Buffer		fc_vmbuffer = InvalidBuffer;
	bool		fc_all_visible_cleared = false;

	/* 简单、廉价的检查元组是否匹配关系的行类型。 */
	Assert(HeapTupleHeaderGetNatts(fc_tup->t_data) <=
		   RelationGetNumberOfAttributes(fc_relation));

	/*
	 * 填充元组头字段，如有必要则对元组进行烤制（toast）。
	 *
	 * 注意：在此之前，heaptup是我们实际打算存储到关系中的数据；
	 * tup是调用者的原始未经烤制的数据。
	 */
	fc_heaptup = fc_heap_prepare_insert(fc_relation, fc_tup, fc_xid, fc_cid, fc_options);

	/*
	 * 找到将此元组插入的缓冲区。如果页面都是可见的，
	 * 这也将固定所需的可见性映射页面。
	 */
	fc_buffer = RelationGetBufferForTuple(fc_relation, fc_heaptup->t_len,
									   InvalidBuffer, fc_options, fc_bistate,
									   &fc_vmbuffer, NULL);

	/*
	 * 我们即将进行实际插入 - 但首先检查冲突，以
	 * 避免可能需要回滚我们刚刚完成的工作。
	 *
	 * 只要在此检查与插入可见之间没有其他进程扫描页面的可能性，
	 * 这在没有再检查的情况下是安全的（即，从此时起，排他性缓冲区内容锁
	 * 会持续持有，直到元组插入变得可见）。
	 *
	 * 对于堆插入，我们只需检查表级SSI锁。我们的
	 * 新元组不可能与现有元组锁冲突，而堆页面锁只是元组锁的合并版本；
	 * 它们不会像索引页面锁那样锁定“间隙”。所以我们在调用时不需要指定一个
	 * 缓冲区，这使得检查更快。
	 */
	CheckForSerializableConflictIn(fc_relation, NULL, InvalidBlockNumber);

	/* 从这里到更改记录时不报告错误 */
	START_CRIT_SECTION();

	RelationPutHeapTuple(fc_relation, fc_buffer, fc_heaptup,
						 (fc_options & HEAP_INSERT_SPECULATIVE) != 0);

	if (PageIsAllVisible(BufferGetPage(fc_buffer)))
	{
		fc_all_visible_cleared = true;
		PageClearAllVisible(BufferGetPage(fc_buffer));
		visibilitymap_clear(fc_relation,
							ItemPointerGetBlockNumber(&(fc_heaptup->t_self)),
							fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
	}

	/*
	 * XXX 我们应该在此页面上设置PageSetPrunable吗？
	 *
	 * 插入的事务可能最终会中止，从而使这个元组
	 * 变为DEAD，因此可以被修剪。尽管我们不想针对中止进行优化，
	 * 如果该页面中没有其他元组被UPDATE或DELETE，这个被中止的元组
	 * 将在下一次触发清理之前永远不会被修剪。
	 *
	 * 如果您在这里添加了PageSetPrunable，也请在heap_xlog_insert中添加。
	 */

	MarkBufferDirty(fc_buffer);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_insert fc_xlrec;
		xl_heap_header fc_xlhdr;
		XLogRecPtr	fc_recptr;
		Page		fc_page = BufferGetPage(fc_buffer);
		uint8		fc_info = XLOG_HEAP_INSERT;
		int			fc_bufflags = 0;

		/*
		 * 如果这是一个目录，我们需要传输组合CID以便正确
		 * 解码，因此也要记录这一点。
		 */
		if (RelationIsAccessibleInLogicalDecoding(fc_relation))
			fc_log_heap_new_cid(fc_relation, fc_heaptup);

		/*
		 * 如果这是页面上的唯一且第一个元组，我们可以重新初始化
		 * 页面，而不是恢复整个内容。设置标志，并隐藏
		 * 来自XLogInsert的缓冲区引用。
		 */
		if (ItemPointerGetOffsetNumber(&(fc_heaptup->t_self)) == FirstOffsetNumber &&
			PageGetMaxOffsetNumber(fc_page) == FirstOffsetNumber)
		{
			fc_info |= XLOG_HEAP_INIT_PAGE;
			fc_bufflags |= REGBUF_WILL_INIT;
		}

		fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_heaptup->t_self);
		fc_xlrec.flags = 0;
		if (fc_all_visible_cleared)
			fc_xlrec.flags |= XLH_INSERT_ALL_VISIBLE_CLEARED;
		if (fc_options & HEAP_INSERT_SPECULATIVE)
			fc_xlrec.flags |= XLH_INSERT_IS_SPECULATIVE;
		Assert(ItemPointerGetBlockNumber(&fc_heaptup->t_self) == BufferGetBlockNumber(fc_buffer));

		/*
		 * 对于逻辑解码，无论我们是否进行全页写入，都需要元组，因此即使我们获取全页图像，也要确保它被包含在内。 (XXX 我们可以选择存储指向 FPW 的指针)。
		 */
		if (RelationIsLogicallyLogged(fc_relation) &&
			!(fc_options & HEAP_INSERT_NO_LOGICAL))
		{
			fc_xlrec.flags |= XLH_INSERT_CONTAINS_NEW_TUPLE;
			fc_bufflags |= REGBUF_KEEP_DATA;

			if (IsToastRelation(fc_relation))
				fc_xlrec.flags |= XLH_INSERT_ON_TOAST_RELATION;
		}

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapInsert);

		fc_xlhdr.t_infomask2 = fc_heaptup->t_data->t_infomask2;
		fc_xlhdr.t_infomask = fc_heaptup->t_data->t_infomask;
		fc_xlhdr.t_hoff = fc_heaptup->t_data->t_hoff;

		/*
		 * 注意我们将 xlhdr 标记为属于缓冲区；如果 XLogInsert 决定将整个页面写入 xlog，那么我们就不需要在 xlog 中存储 xl_heap_header。
		 */
		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD | fc_bufflags);
		XLogRegisterBufData(0, (char *) &fc_xlhdr, SizeOfHeapHeader);
		/* PG73FORMAT: 写入位图 [+ 填充] [+ oid] + 数据 */
		XLogRegisterBufData(0,
							(char *) fc_heaptup->t_data + SizeofHeapTupleHeader,
							fc_heaptup->t_len - SizeofHeapTupleHeader);

		/* 按行级别的来源过滤要高效得多 */
		XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);

		fc_recptr = XLogInsert(RM_HEAP_ID, fc_info);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	UnlockReleaseBuffer(fc_buffer);
	if (fc_vmbuffer != InvalidBuffer)
		ReleaseBuffer(fc_vmbuffer);

	/*
	 * 如果元组是可缓存的，则在我们中止的情况下，将其标记为从缓存中使无效。 注意，在释放缓冲区后这样做是可以的，因为 heaptup 数据结构全部在本地内存中，而不在共享缓冲区中。
	 */
	CacheInvalidateHeapTuple(fc_relation, fc_heaptup, NULL);

	/* 注意：即使后来中止，推测性插入也会被计入 */
	pgstat_count_heap_insert(fc_relation, 1);

	/*
	 * 如果 heaptup 是私有副本，则释放它。 不要忘记将 t_self 复制回调用者的图像中。
	 */
	if (fc_heaptup != fc_tup)
	{
		fc_tup->t_self = fc_heaptup->t_self;
		heap_freetuple(fc_heaptup);
	}
}

/*
 * heap_insert() 的子程序。 准备一个元组以备插入。 这将设置元组头字段，如果需要，则对元组进行 toast。 如果进行了 toast，则返回一个 toast 版的元组；如果没有，则返回原始元组。 请注意，无论如何，头字段也会在原始元组中设置。
 */
static HeapTuple fc_heap_prepare_insert(Relation fc_relation, HeapTuple fc_tup, TransactionId fc_xid,
					CommandId fc_cid, int fc_options)
{
	/*
	 * 为了允许并行插入，我们需要确保它们在工作线程中可以安全执行。 我们有基础设施可以允许一般的并行插入，除了在插入生成新的 CommandId（例如，插入到具有外键列的表）时的情况。
	 */
	if (IsParallelWorker())
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
				 errmsg("cannot insert tuples in a parallel worker")));

	fc_tup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
	fc_tup->t_data->t_infomask2 &= ~(HEAP2_XACT_MASK);
	fc_tup->t_data->t_infomask |= HEAP_XMAX_INVALID;
	HeapTupleHeaderSetXmin(fc_tup->t_data, fc_xid);
	if (fc_options & HEAP_INSERT_FROZEN)
		HeapTupleHeaderSetXminFrozen(fc_tup->t_data);

	HeapTupleHeaderSetCmin(fc_tup->t_data, fc_cid);
	HeapTupleHeaderSetXmax(fc_tup->t_data, 0); /* 为了整洁 */
	fc_tup->t_tableOid = RelationGetRelid(fc_relation);

	/*
	 * 如果新元组太大而无法存储，或包含来自其他关系的已经 toast 的外部属性，则调用 toaster。
	 */
	if (fc_relation->rd_rel->relkind != RELKIND_RELATION &&
		fc_relation->rd_rel->relkind != RELKIND_MATVIEW)
	{
		/* toast 表条目永远不应递归 toast */
		Assert(!HeapTupleHasExternal(fc_tup));
		return fc_tup;
	}
	else if (HeapTupleHasExternal(fc_tup) || fc_tup->t_len > TOAST_TUPLE_THRESHOLD)
		return heap_toast_insert_or_update(fc_relation, fc_tup, NULL, fc_options);
	else
		return fc_tup;
}

/*
 *	heap_multi_insert	- 将多个元组插入堆中
 *
 * 这类似于 heap_insert()，但在一次操作中插入多个元组。 这比在循环中调用 heap_insert() 更快，因为当多个元组可以在单个页面上插入时，我们只需写入一个涵盖所有元组的单个 WAL 记录，并且只需锁定/解锁页面一次。
 *
 * 注意：这会将内存泄漏到当前内存上下文中。如果这是一个问题，您可以在调用此函数之前创建一个临时上下文。
 */
void heap_multi_insert(Relation fc_relation, TupleTableSlot **fc_slots, int fc_ntuples,
				  CommandId fc_cid, int fc_options, BulkInsertState fc_bistate)
{
	TransactionId fc_xid = GetCurrentTransactionId();
	HeapTuple  *fc_heaptuples;
	int			fc_i;
	int			fc_ndone;
	PGAlignedBlock fc_scratch;
	Page		fc_page;
	Buffer		fc_vmbuffer = InvalidBuffer;
	bool		fc_needwal;
	Size		fc_saveFreeSpace;
	bool		fc_need_tuple_data = RelationIsLogicallyLogged(fc_relation);
	bool		fc_need_cids = RelationIsAccessibleInLogicalDecoding(fc_relation);

	/* 当前对于 heap_multi_insert() 不需要（因此不支持） */
	AssertArg(!(fc_options & HEAP_INSERT_NO_LOGICAL));

	fc_needwal = RelationNeedsWAL(fc_relation);
	fc_saveFreeSpace = RelationGetTargetPageFreeSpace(fc_relation,
												   HEAP_DEFAULT_FILLFACTOR);

	/* 在所有槽中进行 toast 和设置头数据 */
	fc_heaptuples = palloc(fc_ntuples * sizeof(HeapTuple));
	for (fc_i = 0; fc_i < fc_ntuples; fc_i++)
	{
		HeapTuple	fc_tuple;

		fc_tuple = ExecFetchSlotHeapTuple(fc_slots[fc_i], true, NULL);
		fc_slots[fc_i]->tts_tableOid = RelationGetRelid(fc_relation);
		fc_tuple->t_tableOid = fc_slots[fc_i]->tts_tableOid;
		fc_heaptuples[fc_i] = fc_heap_prepare_insert(fc_relation, fc_tuple, fc_xid, fc_cid,
											fc_options);
	}

	/*
	 * 我们即将进行实际的插入——但首先检查冲突，以尽量减少需要回滚刚刚完成工作的可能性。
	 *
	 * 此处的检查并不能完全防止序列化异常；该检查必须在获取每个受影响的缓冲区的独占缓冲区内容锁之后进行，并且可以在所有插入反映在缓冲区且这些锁被释放后进行；否则将出现竞争条件。由于多个缓冲区可以在下面的循环中被锁定和解锁，因此在循环之前识别并锁定所有这些缓冲区并不切实际，所以我们必须在最后进行最终检查。
	 *
	 * 此处的检查可以省略，而不会损失正确性；它严格作为一种优化存在。
	 *
	 * 对于堆插入，我们只需要检查表级 SSI 锁。我们的新元组不可能与现有元组锁冲突，而堆页面锁只是元组锁的合并版本；它们不会像索引页面锁那样锁定“间隙”。因此，在进行调用时我们不需要指定缓冲区，这使得检查更快。
	 */
	CheckForSerializableConflictIn(fc_relation, NULL, InvalidBlockNumber);

	fc_ndone = 0;
	while (fc_ndone < fc_ntuples)
	{
		Buffer		fc_buffer;
		bool		fc_starting_with_empty_page;
		bool		fc_all_visible_cleared = false;
		bool		fc_all_frozen_set = false;
		int			fc_nthispage;

		CHECK_FOR_INTERRUPTS();

		/*
		 * 找到至少可以容纳下一个元组的缓冲区。如果页面是全部可见的，这也会固定所需的可见性映射页面。
		 *
		 * 如果 COPY FREEZE 将元组插入到空页面中，也要固定可见性映射页面。见下面的 all_frozen_set。
		 */
		fc_buffer = RelationGetBufferForTuple(fc_relation, fc_heaptuples[fc_ndone]->t_len,
										   InvalidBuffer, fc_options, fc_bistate,
										   &fc_vmbuffer, NULL);
		fc_page = BufferGetPage(fc_buffer);

		fc_starting_with_empty_page = PageGetMaxOffsetNumber(fc_page) == 0;

		if (fc_starting_with_empty_page && (fc_options & HEAP_INSERT_FROZEN))
			fc_all_frozen_set = true;

		/* 从这里到更改记录时不报告错误 */
		START_CRIT_SECTION();

		/*
		 * RelationGetBufferForTuple 已确保第一个元组适合。将其放在页面上，然后将尽可能多的其他元组放上去。
		 */
		RelationPutHeapTuple(fc_relation, fc_buffer, fc_heaptuples[fc_ndone], false);

		/*
		 * 对于逻辑解码，我们需要组合 CID 以正确解码目录。
		 */
		if (fc_needwal && fc_need_cids)
			fc_log_heap_new_cid(fc_relation, fc_heaptuples[fc_ndone]);

		for (fc_nthispage = 1; fc_ndone + fc_nthispage < fc_ntuples; fc_nthispage++)
		{
			HeapTuple	fc_heaptup = fc_heaptuples[fc_ndone + fc_nthispage];

			if (PageGetHeapFreeSpace(fc_page) < MAXALIGN(fc_heaptup->t_len) + fc_saveFreeSpace)
				break;

			RelationPutHeapTuple(fc_relation, fc_buffer, fc_heaptup, false);

			/*
			 * 对于逻辑解码，我们需要组合 CID 以正确解码目录。
			 */
			if (fc_needwal && fc_need_cids)
				fc_log_heap_new_cid(fc_relation, fc_heaptup);
		}

		/*
		 * 如果页面是全部可见的，除非我们只打算向其中添加更多冻结的行，否则需要清除该页面。
		 *
		 * 如果我们只向先前的空页面添加已经冻结的行，则将其标记为全部可见。
		 */
		if (PageIsAllVisible(fc_page) && !(fc_options & HEAP_INSERT_FROZEN))
		{
			fc_all_visible_cleared = true;
			PageClearAllVisible(fc_page);
			visibilitymap_clear(fc_relation,
								BufferGetBlockNumber(fc_buffer),
								fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
		}
		else if (fc_all_frozen_set)
			PageSetAllVisible(fc_page);

		/*
		 * XXX 我们应该在此页面上设置 PageSetPrunable 吗？见 heap_insert()
		 */

		MarkBufferDirty(fc_buffer);

		/* XLOG 相关内容 */
		if (fc_needwal)
		{
			XLogRecPtr	fc_recptr;
			xl_heap_multi_insert *fc_xlrec;
			uint8		fc_info = XLOG_HEAP2_MULTI_INSERT;
			char	   *fc_tupledata;
			int			fc_totaldatalen;
			char	   *fc_scratchptr = fc_scratch.data;
			bool		fc_init;
			int			fc_bufflags = 0;

			/*
			 * 如果页面以前是空的，我们可以重新初始化页面，而不是恢复整个页面。
			 */
			fc_init = fc_starting_with_empty_page;

			/* 从临时区域分配 xl_heap_multi_insert 结构 */
			fc_xlrec = (xl_heap_multi_insert *) fc_scratchptr;
			fc_scratchptr += SizeOfHeapMultiInsert;

			/*
			 * 分配偏移量数组。除非我们正在重新初始化页面，在这种情况下，元组按顺序从 FirstOffsetNumber 开始存储，我们不需要显式存储偏移量。
			 */
			if (!fc_init)
				fc_scratchptr += fc_nthispage * sizeof(OffsetNumber);

			/* 剩余的临时空间用于元组数据 */
			fc_tupledata = fc_scratchptr;

			/* 检查互斥标志是否没有同时设置 */
			Assert(!(fc_all_visible_cleared && fc_all_frozen_set));

			fc_xlrec->flags = 0;
			if (fc_all_visible_cleared)
				fc_xlrec->flags = XLH_INSERT_ALL_VISIBLE_CLEARED;
			if (fc_all_frozen_set)
				fc_xlrec->flags = XLH_INSERT_ALL_FROZEN_SET;

			fc_xlrec->ntuples = fc_nthispage;

			/*
			 * 为每个元组写出一个 xl_multi_insert_tuple 及元组数据本身。
			 */
			for (fc_i = 0; fc_i < fc_nthispage; fc_i++)
			{
				HeapTuple	fc_heaptup = fc_heaptuples[fc_ndone + fc_i];
				xl_multi_insert_tuple *fc_tuphdr;
				int			fc_datalen;

				if (!fc_init)
					fc_xlrec->offsets[fc_i] = ItemPointerGetOffsetNumber(&fc_heaptup->t_self);
				/* xl_multi_insert_tuple 需要两个字节对齐。 */
				fc_tuphdr = (xl_multi_insert_tuple *) SHORTALIGN(fc_scratchptr);
				fc_scratchptr = ((char *) fc_tuphdr) + SizeOfMultiInsertTuple;

				fc_tuphdr->t_infomask2 = fc_heaptup->t_data->t_infomask2;
				fc_tuphdr->t_infomask = fc_heaptup->t_data->t_infomask;
				fc_tuphdr->t_hoff = fc_heaptup->t_data->t_hoff;

				/* 写入位图 [+ 填充] [+ oid] + 数据 */
				fc_datalen = fc_heaptup->t_len - SizeofHeapTupleHeader;
				memcpy(fc_scratchptr,
					   (char *) fc_heaptup->t_data + SizeofHeapTupleHeader,
					   fc_datalen);
				fc_tuphdr->datalen = fc_datalen;
				fc_scratchptr += fc_datalen;
			}
			fc_totaldatalen = fc_scratchptr - fc_tupledata;
			Assert((fc_scratchptr - fc_scratch.data) < BLCKSZ);

			if (fc_need_tuple_data)
				fc_xlrec->flags |= XLH_INSERT_CONTAINS_NEW_TUPLE;

			/*
			 * 信号指示这是通过此调用heap_multi_insert()发出的最后一个xl_heap_multi_insert记录。
			 * 这对于逻辑解码是必要的，以便知道何时清理临时数据。
			 */
			if (fc_ndone + fc_nthispage == fc_ntuples)
				fc_xlrec->flags |= XLH_INSERT_LAST_IN_MULTI;

			if (fc_init)
			{
				fc_info |= XLOG_HEAP_INIT_PAGE;
				fc_bufflags |= REGBUF_WILL_INIT;
			}

			/*
			 * 如果我们正在进行逻辑解码，即使我们对页面进行完整页面映像，也要包含新的元组数据。
			 */
			if (fc_need_tuple_data)
				fc_bufflags |= REGBUF_KEEP_DATA;

			XLogBeginInsert();
			XLogRegisterData((char *) fc_xlrec, fc_tupledata - fc_scratch.data);
			XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD | fc_bufflags);

			XLogRegisterBufData(0, fc_tupledata, fc_totaldatalen);

			/* 按行级别的来源过滤要高效得多 */
			XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);

			fc_recptr = XLogInsert(RM_HEAP2_ID, fc_info);

			PageSetLSN(fc_page, fc_recptr);
		}

		END_CRIT_SECTION();

		/*
			 * 如果我们已经冻结了页面上的所有内容，请更新可见性映射。
			 * 我们已经在vmbuffer上保持针脚。
			 */
		if (fc_all_frozen_set)
		{
			Assert(PageIsAllVisible(fc_page));
			Assert(visibilitymap_pin_ok(BufferGetBlockNumber(fc_buffer), fc_vmbuffer));

			/*
			 * 在这里使用InvalidTransactionId是可以的 - 这仅在指定HEAP_INSERT_FROZEN时使用，
			 * 这故意违反可见性规则。
			 */
			visibilitymap_set(fc_relation, BufferGetBlockNumber(fc_buffer), fc_buffer,
							  InvalidXLogRecPtr, fc_vmbuffer,
							  InvalidTransactionId,
							  VISIBILITYMAP_ALL_VISIBLE | VISIBILITYMAP_ALL_FROZEN);
		}

		UnlockReleaseBuffer(fc_buffer);
		fc_ndone += fc_nthispage;

		/*
			 * 注意：仅在插入所有元组后释放vmbuffer - 我们很可能会插入到
			 * 可能使用相同vm页面的后续堆页面。
			 */
	}

	/* 我们已经完成了所有元组的插入，因此释放最后一个vmbuffer。 */
	if (fc_vmbuffer != InvalidBuffer)
		ReleaseBuffer(fc_vmbuffer);

	/*
	 * 我们已经完成了实际的插入。再次检查冲突，以确保
	 * 检测到所有rw冲突。如果没有这个最后的检查，堆的顺序扫描
	 * 可能在“之前”检查后锁定了表，错过了检测
	 * 冲突的机会，然后在新元组存在之前扫描了表，
	 * 错过了检测冲突的另一个机会。
	 *
	 * 对于堆插入，我们只需要检查表级SSI锁。我们的
	 * 新元组不可能与现有元组锁冲突，并且堆
	 * 页面锁只是元组锁的合并版本；它们不会锁定
	 * "间隙"，就像索引页面锁那样。因此，我们不需要在
	 * 调用时指定一个缓冲区。
	 */
	CheckForSerializableConflictIn(fc_relation, NULL, InvalidBlockNumber);

	/*
	 * 如果元组可以缓存，则标记它们以防我们中止时从缓存中失效。
	 * 注意，在释放缓冲区后这样做是可以的，因为
	 * heaptuples数据结构全部在本地内存中，而不在
	 * 共享缓冲区中。
	 */
	if (IsCatalogRelation(fc_relation))
	{
		for (fc_i = 0; fc_i < fc_ntuples; fc_i++)
			CacheInvalidateHeapTuple(fc_relation, fc_heaptuples[fc_i], NULL);
	}

	/* 将t_self字段复制回调用者的槽 */
	for (fc_i = 0; fc_i < fc_ntuples; fc_i++)
		fc_slots[fc_i]->tts_tid = fc_heaptuples[fc_i]->t_self;

	pgstat_count_heap_insert(fc_relation, fc_ntuples);
}

/*
 *	simple_heap_insert - 插入一个元组
 *
 * 目前，此例程与heap_insert的不同之处在于提供了一个默认的命令ID，并且不允许访问加速选项。
 *
 * 在大多数修改系统目录的地方，这应该优先使用，而不是直接使用heap_insert。
 */
void simple_heap_insert(Relation fc_relation, HeapTuple fc_tup)
{
	heap_insert(fc_relation, fc_tup, GetCurrentCommandId(true), 0, NULL);
}

/*
 * 给定infomask/infomask2，计算必须保存的位
 * 在xl_heap_delete、xl_heap_update、xl_heap_lock、
 * xl_heap_lock_updated WAL记录的“infobits”字段中。
 *
 * 参见fix_infomask_from_infobits。
 */
static uint8 fc_compute_infobits(uint16 fc_infomask, uint16 fc_infomask2)
{
	return
		((fc_infomask & HEAP_XMAX_IS_MULTI) != 0 ? XLHL_XMAX_IS_MULTI : 0) |
		((fc_infomask & HEAP_XMAX_LOCK_ONLY) != 0 ? XLHL_XMAX_LOCK_ONLY : 0) |
		((fc_infomask & HEAP_XMAX_EXCL_LOCK) != 0 ? XLHL_XMAX_EXCL_LOCK : 0) |
	/* 注意我们在这里忽略了HEAP_XMAX_SHR_LOCK */
		((fc_infomask & HEAP_XMAX_KEYSHR_LOCK) != 0 ? XLHL_XMAX_KEYSHR_LOCK : 0) |
		((fc_infomask2 & HEAP_KEYS_UPDATED) != 0 ?
		 XLHL_KEYS_UPDATED : 0);
}

/*
 * 给定一个元组的两个版本 t_infomask，对它们进行比较，并返回元组 Xmax 的相关状态是否发生了变化。该过程在释放并重新获取缓冲区锁后使用：我们希望确保元组状态在我们之前检查时保持不变。
 *
 * 注意，Xmax 字段本身必须单独比较。
 */
static inline bool fc_xmax_infomask_changed(uint16 fc_new_infomask, uint16 fc_old_infomask)
{
	const uint16 fc_interesting =
	HEAP_XMAX_IS_MULTI | HEAP_XMAX_LOCK_ONLY | HEAP_LOCK_MASK;

	if ((fc_new_infomask & fc_interesting) != (fc_old_infomask & fc_interesting))
		return true;

	return false;
}

/*
 *	heap_delete - 删除一个元组
 *
 * 有关参数的解释，请参见 table_tuple_delete()，只不过该例程直接处理元组而不是插槽。
 *
 * 在失败的情况下，该例程用元组的 t_ctid、t_xmax（如有必要，解决可能的 MultiXact）和 t_cmax 填充 *tmfd（最后一个仅适用于 TM_SelfModified，因为我们不能从其他事务生成的组合 CID 中获取 cmax）。
 */
TM_Result heap_delete(Relation fc_relation, ItemPointer fc_tid,
			CommandId fc_cid, Snapshot fc_crosscheck, bool fc_wait,
			TM_FailureData *fc_tmfd, bool fc_changingPart)
{
	TM_Result	fc_result;
	TransactionId fc_xid = GetCurrentTransactionId();
	ItemId		fc_lp;
	HeapTupleData fc_tp;
	Page		fc_page;
	BlockNumber fc_block;
	Buffer		fc_buffer;
	Buffer		fc_vmbuffer = InvalidBuffer;
	TransactionId fc_new_xmax;
	uint16		fc_new_infomask,
				fc_new_infomask2;
	bool		fc_have_tuple_lock = false;
	bool		fc_iscombo;
	bool		fc_all_visible_cleared = false;
	HeapTuple	fc_old_key_tuple = NULL;	/* 元组的复制标识 */
	bool		fc_old_key_copied = false;

	Assert(ItemPointerIsValid(fc_tid));

	/*
	 * 在并行操作期间禁止此操作，以免分配组合 CID。
	 * 其他工作者可能需要该组合 CID 进行可见性检查，我们没有向他们广播的规定。
	 */
	if (IsInParallelMode())
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
				 errmsg("cannot delete tuples during a parallel operation")));

	fc_block = ItemPointerGetBlockNumber(fc_tid);
	fc_buffer = ReadBuffer(fc_relation, fc_block);
	fc_page = BufferGetPage(fc_buffer);

	/*
	 * 在锁定缓冲区之前，如果显得有必要，则固定可见性映射页面。
	 * 由于我们还没有获得锁，其他人可能正在更改它，因此在我们获得锁后需要重新检查。
	 */
	if (PageIsAllVisible(fc_page))
		visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);

	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

	fc_lp = PageGetItemId(fc_page, ItemPointerGetOffsetNumber(fc_tid));
	Assert(ItemIdIsNormal(fc_lp));

	fc_tp.t_tableOid = RelationGetRelid(fc_relation);
	fc_tp.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);
	fc_tp.t_len = ItemIdGetLength(fc_lp);
	fc_tp.t_self = *fc_tid;

l1:
	/*
	 * 如果我们没有固定可见性映射页面，而在我们忙于锁定缓冲区时该页面变得全部可见，我们将不得不解锁并重新锁定，以避免在 I/O 操作中持有缓冲区锁。这有点不幸，但希望不会经常发生。
	 */
	if (fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page))
	{
		LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
		visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);
		LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
	}

	fc_result = HeapTupleSatisfiesUpdate(&fc_tp, fc_cid, fc_buffer);

	if (fc_result == TM_Invisible)
	{
		UnlockReleaseBuffer(fc_buffer);
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("attempted to delete invisible tuple")));
	}
	else if (fc_result == TM_BeingModified && fc_wait)
	{
		TransactionId fc_xwait;
		uint16		fc_infomask;

		/* 在解锁缓冲区之前必须复制状态数据 */
		fc_xwait = HeapTupleHeaderGetRawXmax(fc_tp.t_data);
		fc_infomask = fc_tp.t_data->t_infomask;

		/*
		 * 睡眠直到并发事务结束——除了当只有一个锁定者且是我们自己的事务时。注意我们不关心锁定者的锁模式，因为我们需要最强的那个。
		 *
		 * 在休眠之前，我们需要获取元组锁以建立我们对元组的优先权（参见 heap_lock_tuple）。当我们在元组中排队时，LockTuple 将释放我们。
		 *
		 * 如果我们被迫在下面“重新开始”，我们保留元组锁；这确保我们在重新检查元组状态时保持在队列的最前面。
		 */
		if (fc_infomask & HEAP_XMAX_IS_MULTI)
		{
			bool		fc_current_is_member = false;

			if (fc_DoesMultiXactIdConflict((MultiXactId) fc_xwait, fc_infomask,
										LockTupleExclusive, &fc_current_is_member))
			{
				LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

				/*
				 * 如果有必要，获取锁（但在请求锁并且已经拥有锁时跳过；避免死锁）。
				 */
				if (!fc_current_is_member)
					fc_heap_acquire_tuplock(fc_relation, &(fc_tp.t_self), LockTupleExclusive,
										 LockWaitBlock, &fc_have_tuple_lock);

				/* 等待多重事务 */
				fc_MultiXactIdWait((MultiXactId) fc_xwait, MultiXactStatusUpdate, fc_infomask,
								fc_relation, &(fc_tp.t_self), XLTW_Delete,
								NULL);
				LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

				/*
				 * 如果 xwait 刚刚锁定了元组，那么在我们到达这一点之前，其他一些 xact 可能会更新该元组。检查 xmax 变化，如果是这样则重新开始。
				 *
				 * 如果我们没有固定 VM 页面，并且页面变得全部可见，我们也必须重新开始。
				 */
				if ((fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page)) ||
					fc_xmax_infomask_changed(fc_tp.t_data->t_infomask, fc_infomask) ||
					!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tp.t_data),
										 fc_xwait))
					goto l1;
			}

			/*
			 * 你可能认为这里的 multixact 是必然完成的，但事实并非如此：它可能还有剩余成员，即我们自己的 xact 或者该后端的其他 subxacts。无论哪种情况，删除元组都是合法的（后者实际上是将我们先前的共享锁升级为独占锁的情况）。我们不必更改磁盘上的提示位，因为我们即将完全覆盖 xmax。
			 */
		}
		else if (!TransactionIdIsCurrentTransactionId(fc_xwait))
		{
			/*
			 * 等待常规事务结束；但首先，获取元组锁。
			 */
			LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			fc_heap_acquire_tuplock(fc_relation, &(fc_tp.t_self), LockTupleExclusive,
								 LockWaitBlock, &fc_have_tuple_lock);
			XactLockTableWait(fc_xwait, fc_relation, &(fc_tp.t_self), XLTW_Delete);
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

			/*
			 * xwait 已完成，但如果 xwait 刚锁定了元组，那么其他 xact 可能会在我们到达这一点之前更新该元组。检查 xmax 的变化，如果有则重新开始。
			 *
			 * 如果我们没有固定 VM 页，并且页面已经变得完全可见，我们也必须重新开始。
			 */
			if ((fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page)) ||
				fc_xmax_infomask_changed(fc_tp.t_data->t_infomask, fc_infomask) ||
				!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tp.t_data),
									 fc_xwait))
				goto l1;

			/* 否则检查它是提交还是中止 */
			fc_UpdateXmaxHintBits(fc_tp.t_data, fc_buffer, fc_xwait);
		}

		/*
		 * 如果先前的 xmax 中止，或者它已提交但仅锁定了元组而未更新，则我们可以覆盖。
		 */
		if ((fc_tp.t_data->t_infomask & HEAP_XMAX_INVALID) ||
			HEAP_XMAX_IS_LOCKED_ONLY(fc_tp.t_data->t_infomask) ||
			HeapTupleHeaderIsOnlyLocked(fc_tp.t_data))
			fc_result = TM_Ok;
		else if (!ItemPointerEquals(&fc_tp.t_self, &fc_tp.t_data->t_ctid))
			fc_result = TM_Updated;
		else
			fc_result = TM_Deleted;
	}

	/* 对结果 HeapTupleSatisfiesUpdate() 和上述逻辑进行健全性检查 */
	if (fc_result != TM_Ok)
	{
		Assert(fc_result == TM_SelfModified ||
			   fc_result == TM_Updated ||
			   fc_result == TM_Deleted ||
			   fc_result == TM_BeingModified);
		Assert(!(fc_tp.t_data->t_infomask & HEAP_XMAX_INVALID));
		Assert(fc_result != TM_Updated ||
			   !ItemPointerEquals(&fc_tp.t_self, &fc_tp.t_data->t_ctid));
	}

	if (fc_crosscheck != InvalidSnapshot && fc_result == TM_Ok)
	{
		/* 对事务快照模式 RI 更新进行额外检查 */
		if (!HeapTupleSatisfiesVisibility(&fc_tp, fc_crosscheck, fc_buffer))
			fc_result = TM_Updated;
	}

	if (fc_result != TM_Ok)
	{
		fc_tmfd->ctid = fc_tp.t_data->t_ctid;
		fc_tmfd->xmax = HeapTupleHeaderGetUpdateXid(fc_tp.t_data);
		if (fc_result == TM_SelfModified)
			fc_tmfd->cmax = HeapTupleHeaderGetCmax(fc_tp.t_data);
		else
			fc_tmfd->cmax = InvalidCommandId;
		UnlockReleaseBuffer(fc_buffer);
		if (fc_have_tuple_lock)
			UnlockTupleTuplock(fc_relation, &(fc_tp.t_self), LockTupleExclusive);
		if (fc_vmbuffer != InvalidBuffer)
			ReleaseBuffer(fc_vmbuffer);
		return fc_result;
	}

	/*
	 * 我们即将进行实际删除——首先检查冲突，以避免可能需要回滚我们刚刚完成的工作。
	 *
	 * 只要在此检查与删除可见之间没有其他进程扫描该页面的可能性，这在没有重新检查的情况下是安全的（即，从这一点开始，独占缓冲区内容锁会持续保持到元组删除可见）。
	 */
	CheckForSerializableConflictIn(fc_relation, fc_tid, BufferGetBlockNumber(fc_buffer));

	/* 如有必要，用组合 CID 替换 cid */
	HeapTupleHeaderAdjustCmax(fc_tp.t_data, &fc_cid, &fc_iscombo);

	/*
	 * 在进入临界区之前计算副本身份元组，以便在内存分配失败时不至于引发 PANIC。
	 */
	fc_old_key_tuple = fc_ExtractReplicaIdentity(fc_relation, &fc_tp, true, &fc_old_key_copied);

	/*
	 * 如果这是当前事务中的第一次可能的 multixact 操作，则设置我每个后端的 OldestMemberMXactId 设置。我们可以确定该事务绝不会成为任何旧的 MultiXactIds 的成员。（即使我们最终只是使用下面自己的 TransactionId，我们也必须这样做，因为其他某个后端可能会在之后立即将我们的 XID 纳入 MultiXact。）
	 */
	MultiXactIdSetOldestMember();

	fc_compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(fc_tp.t_data),
							  fc_tp.t_data->t_infomask, fc_tp.t_data->t_infomask2,
							  fc_xid, LockTupleExclusive, true,
							  &fc_new_xmax, &fc_new_infomask, &fc_new_infomask2);

	START_CRIT_SECTION();

	/*
	 * 如果该事务提交，元组迟早会变为 DEAD。设置标志，这个页面是一旦我们的 xid 低于 OldestXmin 阈值就可以修剪的候选者。如果事务最终中止，后续的页面修剪将是空操作，提示将被清除。
	 */
	PageSetPrunable(fc_page, fc_xid);

	if (PageIsAllVisible(fc_page))
	{
		fc_all_visible_cleared = true;
		PageClearAllVisible(fc_page);
		visibilitymap_clear(fc_relation, BufferGetBlockNumber(fc_buffer),
							fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
	}

	/* 存储删除元组的 xact 的事务信息 */
	fc_tp.t_data->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
	fc_tp.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED;
	fc_tp.t_data->t_infomask |= fc_new_infomask;
	fc_tp.t_data->t_infomask2 |= fc_new_infomask2;
	HeapTupleHeaderClearHotUpdated(fc_tp.t_data);
	HeapTupleHeaderSetXmax(fc_tp.t_data, fc_new_xmax);
	HeapTupleHeaderSetCmax(fc_tp.t_data, fc_cid, fc_iscombo);
	/* 确保 t_ctid 中没有向前链路 */
	fc_tp.t_data->t_ctid = fc_tp.t_self;

	/* 表示这实际上是移动到另一个分区 */
	if (fc_changingPart)
		HeapTupleHeaderSetMovedPartitions(fc_tp.t_data);

	MarkBufferDirty(fc_buffer);

	/*
	 * XLOG 相关
	 *
	 * 注意：heap_abort_speculative() 使用相同的 xlog 记录和重放例程。
	 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_delete fc_xlrec;
		xl_heap_header fc_xlhdr;
		XLogRecPtr	fc_recptr;

		/*
		 * 对于逻辑解码，我们需要组合CID来正确解码
		 * 目录
		 */
		if (RelationIsAccessibleInLogicalDecoding(fc_relation))
			fc_log_heap_new_cid(fc_relation, &fc_tp);

		fc_xlrec.flags = 0;
		if (fc_all_visible_cleared)
			fc_xlrec.flags |= XLH_DELETE_ALL_VISIBLE_CLEARED;
		if (fc_changingPart)
			fc_xlrec.flags |= XLH_DELETE_IS_PARTITION_MOVE;
		fc_xlrec.infobits_set = fc_compute_infobits(fc_tp.t_data->t_infomask,
											  fc_tp.t_data->t_infomask2);
		fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_tp.t_self);
		fc_xlrec.xmax = fc_new_xmax;

		if (fc_old_key_tuple != NULL)
		{
			if (fc_relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
				fc_xlrec.flags |= XLH_DELETE_CONTAINS_OLD_TUPLE;
			else
				fc_xlrec.flags |= XLH_DELETE_CONTAINS_OLD_KEY;
		}

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapDelete);

		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);

		/*
		 * 如果有被删除的元组，记录下其日志副本身份
		 */
		if (fc_old_key_tuple != NULL)
		{
			fc_xlhdr.t_infomask2 = fc_old_key_tuple->t_data->t_infomask2;
			fc_xlhdr.t_infomask = fc_old_key_tuple->t_data->t_infomask;
			fc_xlhdr.t_hoff = fc_old_key_tuple->t_data->t_hoff;

			XLogRegisterData((char *) &fc_xlhdr, SizeOfHeapHeader);
			XLogRegisterData((char *) fc_old_key_tuple->t_data
							 + SizeofHeapTupleHeader,
							 fc_old_key_tuple->t_len
							 - SizeofHeapTupleHeader);
		}

		/* 按行级别的来源过滤要高效得多 */
		XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);

		fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_DELETE);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

	if (fc_vmbuffer != InvalidBuffer)
		ReleaseBuffer(fc_vmbuffer);

	/*
	 * 如果元组具有被压缩的外部行属性，我们还需要删除
	 * 这些项目。我们必须在释放缓冲区之前这样做，因为我们
	 * 需要查看元组的内容，但可以先释放缓冲区的内容锁。
	 */
	if (fc_relation->rd_rel->relkind != RELKIND_RELATION &&
		fc_relation->rd_rel->relkind != RELKIND_MATVIEW)
	{
		/* toast 表条目永远不应递归 toast */
		Assert(!HeapTupleHasExternal(&fc_tp));
	}
	else if (HeapTupleHasExternal(&fc_tp))
		heap_toast_delete(fc_relation, &fc_tp, false);

	/*
	 * 标记元组在下一个命令边界从系统缓存中无效。我们必须在
	 * 释放缓冲区之前这样做，因为我们需要查看元组的内容。
	 */
	CacheInvalidateHeapTuple(fc_relation, &fc_tp, NULL);

	/* 现在我们可以释放缓冲区 */
	ReleaseBuffer(fc_buffer);

	/*
	 * 释放lmgr元组锁，如果我们有的话。
	 */
	if (fc_have_tuple_lock)
		UnlockTupleTuplock(fc_relation, &(fc_tp.t_self), LockTupleExclusive);

	pgstat_count_heap_delete(fc_relation);

	if (fc_old_key_tuple != NULL && fc_old_key_copied)
		heap_freetuple(fc_old_key_tuple);

	return TM_Ok;
}

/*
 *	simple_heap_delete - 删除一个元组
 *
 * 此例程可用于在不期望对目标元组进行并发更新时删除元组
 * （例如，由于我们对与元组关联的关系加了锁）。任何失败都会通过
 * ereport()报告。
 */
void simple_heap_delete(Relation fc_relation, ItemPointer fc_tid)
{
	TM_Result	fc_result;
	TM_FailureData fc_tmfd;

	fc_result = heap_delete(fc_relation, fc_tid,
						 GetCurrentCommandId(true), InvalidSnapshot,
						 true /* 等待提交 */ ,
						 &fc_tmfd, false /* changingPart */ );
	switch (fc_result)
	{
		case TM_SelfModified:
			/* 元组在当前命令中已经被更新？ */
			elog(ERROR, "tuple already updated by self");
			break;

		case TM_Ok:
			/* 成功完成 */
			break;

		case TM_Updated:
			elog(ERROR, "tuple concurrently updated");
			break;

		case TM_Deleted:
			elog(ERROR, "tuple concurrently deleted");
			break;

		default:
			elog(ERROR, "unrecognized heap_delete status: %u", fc_result);
			break;
	}
}

/*
 *	heap_update - 替换一个元组
 *
 * 有关参数的说明，请参见table_tuple_update()，只不过
 * 此例程直接接受一个元组而不是一个槽。
 *
 * 在失败情况下，该例程将元组的t_ctid、t_xmax（如有必要，解决可能的MultiXact）
 * 和t_cmax填充到*tmfd中（最后一个仅适用于TM_SelfModified，因为
 * 我们无法从其他事务生成的组合CID中获得cmax）。
 */
TM_Result heap_update(Relation fc_relation, ItemPointer fc_otid, HeapTuple fc_newtup,
			CommandId fc_cid, Snapshot fc_crosscheck, bool fc_wait,
			TM_FailureData *fc_tmfd, LockTupleMode *fc_lockmode)
{
	TM_Result	fc_result;
	TransactionId fc_xid = GetCurrentTransactionId();
	Bitmapset  *fc_hot_attrs;
	Bitmapset  *fc_key_attrs;
	Bitmapset  *fc_id_attrs;
	Bitmapset  *fc_interesting_attrs;
	Bitmapset  *fc_modified_attrs;
	ItemId		fc_lp;
	HeapTupleData fc_oldtup;
	HeapTuple	fc_heaptup;
	HeapTuple	fc_old_key_tuple = NULL;
	bool		fc_old_key_copied = false;
	Page		fc_page;
	BlockNumber fc_block;
	MultiXactStatus fc_mxact_status;
	Buffer		fc_buffer,
				fc_newbuf,
				fc_vmbuffer = InvalidBuffer,
				fc_vmbuffer_new = InvalidBuffer;
	bool		fc_need_toast;
	Size		fc_newtupsize,
				fc_pagefree;
	bool		fc_have_tuple_lock = false;
	bool		fc_iscombo;
	bool		fc_use_hot_update = false;
	bool		fc_key_intact;
	bool		fc_all_visible_cleared = false;
	bool		fc_all_visible_cleared_new = false;
	bool		fc_checked_lockers;
	bool		fc_locker_remains;
	bool		fc_id_has_external = false;
	TransactionId fc_xmax_new_tuple,
				fc_xmax_old_tuple;
	uint16		fc_infomask_old_tuple,
				fc_infomask2_old_tuple,
				fc_infomask_new_tuple,
				fc_infomask2_new_tuple;

	Assert(ItemPointerIsValid(fc_otid));

	/* 简单、廉价的检查元组是否匹配关系的行类型。 */
	Assert(HeapTupleHeaderGetNatts(fc_newtup->t_data) <=
		   RelationGetNumberOfAttributes(fc_relation));

	/*
	 * 在并行操作期间禁止此操作，以免分配组合 CID。
	 * 其他工作者可能需要该组合 CID 进行可见性检查，我们没有向他们广播的规定。
	 */
	if (IsInParallelMode())
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
				 errmsg("cannot update tuples during a parallel operation")));

#ifdef USE_ASSERT_CHECKING
	fc_check_lock_if_inplace_updateable_rel(fc_relation, fc_otid, fc_newtup);
#endif

	/*
	 * 获取要检查的属性列表，以便进行各种操作。
	 *
	 * 对于HOT的考虑，如果我们未能更新或必须将新元组放在
	 * 不同的页面上，这将是浪费的努力。但我们在获取缓冲区
	 * 锁之前必须计算出该列表——在最坏的情况下，如果我们正在
	 * 更新其中一个相关的系统目录，如果我们稍后尝试获取
	 * 列表，可能会发生死锁。无论如何，relcache
	 * 刷新数据，因此这通常是相当便宜的。
	 *
	 * 我们还需要副本身份使用的列以及被视为表中
	 * 行的“键”的列。
	 *
	 * 请注意，我们会获得每个位图的副本，因此无需担心
	 * relcache刷新在中途发生。
	 */
	fc_hot_attrs = RelationGetIndexAttrBitmap(fc_relation, INDEX_ATTR_BITMAP_ALL);
	fc_key_attrs = RelationGetIndexAttrBitmap(fc_relation, INDEX_ATTR_BITMAP_KEY);
	fc_id_attrs = RelationGetIndexAttrBitmap(fc_relation,
										  INDEX_ATTR_BITMAP_IDENTITY_KEY);
	fc_interesting_attrs = NULL;
	fc_interesting_attrs = bms_add_members(fc_interesting_attrs, fc_hot_attrs);
	fc_interesting_attrs = bms_add_members(fc_interesting_attrs, fc_key_attrs);
	fc_interesting_attrs = bms_add_members(fc_interesting_attrs, fc_id_attrs);

	fc_block = ItemPointerGetBlockNumber(fc_otid);
	fc_buffer = ReadBuffer(fc_relation, fc_block);
	fc_page = BufferGetPage(fc_buffer);

	/*
	 * 在锁定缓冲区之前，如果显得有必要，则固定可见性映射页面。
	 * 由于我们还没有获得锁，其他人可能正在更改它，因此在我们获得锁后需要重新检查。
	 */
	if (PageIsAllVisible(fc_page))
		visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);

	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

	fc_lp = PageGetItemId(fc_page, ItemPointerGetOffsetNumber(fc_otid));

	/*
	 * 通常，缓冲区固定和/或快照会阻止 otid 的修剪，确保我们在此处看到 LP_NORMAL。当 otid 来源于 syscache 时，我们可能既没有固定也没有快照。因此，我们可能会看到其他 LP_ 状态， 每种状态都表示并发修剪。
	 *
	 * 失败时返回 TM_Updated 是最准确的。然而，与其他 TM_Updated 场景不同，我们不知道 LP_UNUSED 和 LP_DEAD 情况下的后续 ctid。虽然 TM_Updated 和 TM_Deleted 之间的区别确实对 SQL 语句 UPDATE 和 MERGE 重要，但这些 SQL 语句持有确保 LP_NORMAL 的快照。因此，TM_Updated 和 TM_Deleted 之间的选择仅影响错误消息的措辞。选择 TM_Deleted 有两个原因。首先，避免了使 tmfd->ctid 的有效性规范复杂化。其次，生成错误日志证据，证明我们采取了这一分支。
	 *
	 * 由于在 otid 处可能看到 LP_UNUSED，因此也可能看到替换 LP_UNUSED 的元组的 LP_NORMAL。如果它是一个与行无关的元组，我们将失败并显示“重复键值违反唯一性”。XXX 如果 otid 是新元组行的活动新版本，我们将丢弃来自 syscache 的版本后该目录行版本的更改。请参见 syscache-update-pruned.spec。
	 */
	if (!ItemIdIsNormal(fc_lp))
	{
		Assert(RelationSupportsSysCache(RelationGetRelid(fc_relation)));

		UnlockReleaseBuffer(fc_buffer);
		Assert(!fc_have_tuple_lock);
		if (fc_vmbuffer != InvalidBuffer)
			ReleaseBuffer(fc_vmbuffer);
		fc_tmfd->ctid = *fc_otid;
		fc_tmfd->xmax = InvalidTransactionId;
		fc_tmfd->cmax = InvalidCommandId;

		bms_free(fc_hot_attrs);
		bms_free(fc_key_attrs);
		bms_free(fc_id_attrs);
		/* modified_attrs 尚未初始化 */
		bms_free(fc_interesting_attrs);
		return TM_Deleted;
	}

	/*
	 * 在 oldtup 中填入足够的数据以便 HeapDetermineColumnsInfo 能正常工作。
	 */
	fc_oldtup.t_tableOid = RelationGetRelid(fc_relation);
	fc_oldtup.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);
	fc_oldtup.t_len = ItemIdGetLength(fc_lp);
	fc_oldtup.t_self = *fc_otid;

	/* 新元组已准备好，除了这一点： */
	fc_newtup->t_tableOid = RelationGetRelid(fc_relation);

	/*
	 * 确定更新时修改的列。此外，识别旧元组中是否有任何未修改的副本标识键属性是外部存储的。这是必需的，因为对于这样的属性，扁平化的值不会作为新元组的一部分被 WAL 记录，因此我们必须将其包含在 old_key_tuple 中。请参见 ExtractReplicaIdentity。
	 */
	fc_modified_attrs = fc_HeapDetermineColumnsInfo(fc_relation, fc_interesting_attrs,
											  fc_id_attrs, &fc_oldtup,
											  fc_newtup, &fc_id_has_external);

	/*
	 * 如果我们没有更新任何“键”列，我们可以采取较弱的锁类型。这允许在与外键检查同时运行时有更多的并发性。
	 *
	 * 注意，如果在执行更新时某列被解压，但值最终是相同的，这个测试将失败，我们将使用更强的锁。这是可以接受的；需要优化的重要情况是不会操作键列的更新，而不是那些偶然到达相同键值的更新。
	 */
	if (!bms_overlap(fc_modified_attrs, fc_key_attrs))
	{
		*fc_lockmode = LockTupleNoKeyExclusive;
		fc_mxact_status = MultiXactStatusNoKeyUpdate;
		fc_key_intact = true;

		/*
		 * 如果这是当前事务中第一个可能的多事务操作，则设置我的每个后端的 OldestMemberMXactId
		 * 设置。我们可以确定该事务永远不会成为任何比这更旧的 MultiXactIds 的成员。
		 * （即使我们最终只使用下面自己的 TransactionId，我们也必须这样做，因为
		 * 其他后端可能会在此之后立即将我们的 XID 合并到一个 MultiXact 中。）
		 */
		MultiXactIdSetOldestMember();
	}
	else
	{
		*fc_lockmode = LockTupleExclusive;
		fc_mxact_status = MultiXactStatusUpdate;
		fc_key_intact = false;
	}

	/*
	 * 注意：在此之后，请使用 oldtup 而不是 otid 来引用旧元组。
	 * otid 很可能指向 newtup->t_self，我们将用新元组的位置覆盖它，
	 * 因此如果我们再使用 otid，会存在很大的混淆风险。
	 */

l2:
	fc_checked_lockers = false;
	fc_locker_remains = false;
	fc_result = HeapTupleSatisfiesUpdate(&fc_oldtup, fc_cid, fc_buffer);

	/* 关于“无等待”情况请参见下面的内容 */
	Assert(fc_result != TM_BeingModified || fc_wait);

	if (fc_result == TM_Invisible)
	{
		UnlockReleaseBuffer(fc_buffer);
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("attempted to update invisible tuple")));
	}
	else if (fc_result == TM_BeingModified && fc_wait)
	{
		TransactionId fc_xwait;
		uint16		fc_infomask;
		bool		fc_can_continue = false;

		/*
		 * XXX 注意我们在这里不考虑“无等待”情况。目前这不是一个问题，
		 * 因为没有调用者使用这种情况，但如果引入这样的调用者，应该修复它。
		 * 以前这不是一个问题，因为这段代码会一直等待，但现在某些
		 * 元组锁与我们使用的锁模式不冲突，这种情况可能值得特别处理。
		 *
		 * 这可能导致直接调用 heap_update 的第三方代码失败。
		 */

		/* 在解锁缓冲区之前必须复制状态数据 */
		fc_xwait = HeapTupleHeaderGetRawXmax(fc_oldtup.t_data);
		fc_infomask = fc_oldtup.t_data->t_infomask;

		/*
		 * 现在我们必须处理现有的锁定者。如果它是一个多事务，就在上面睡眠；
		 * 我们可能在它完全消失之前被唤醒（或者在某些情况下甚至不睡觉）；
		 * 我们需要将其保留为锁定者，除非它完全消失。
		 *
		 * 如果它不是一个多事务，我们需要在实际上睡眠之前检查睡眠条件。
		 * 如果更新不与锁冲突，我们可以继续而无需睡眠（但要确保
		 * 它被保留）。
		 *
		 * 在睡眠之前，我们需要获得元组锁以建立我们对元组的优先级
		 * （参见 heap_lock_tuple）。LockTuple 会在我们按顺序获得
		 * 元组时释放我们。请注意，直到我们确信要睡眠之前，不得获得元组锁；
		 * 否则，我们可能会与其他持有元组锁并在我们身上睡觉的事务发生竞争条件。
		 *
		 * 如果我们被迫在下面“重新开始”，我们将保留元组锁；
		 * 这安排了我们在重新检查元组状态时保持在队列的前面。
		 */
		if (fc_infomask & HEAP_XMAX_IS_MULTI)
		{
			TransactionId fc_update_xact;
			int			fc_remain;
			bool		fc_current_is_member = false;

			if (fc_DoesMultiXactIdConflict((MultiXactId) fc_xwait, fc_infomask,
										*fc_lockmode, &fc_current_is_member))
			{
				LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

				/*
				 * 如果有必要，获取锁（但在请求锁并且已经拥有锁时跳过；避免死锁）。
				 */
				if (!fc_current_is_member)
					fc_heap_acquire_tuplock(fc_relation, &(fc_oldtup.t_self), *fc_lockmode,
										 LockWaitBlock, &fc_have_tuple_lock);

				/* 等待多重事务 */
				fc_MultiXactIdWait((MultiXactId) fc_xwait, fc_mxact_status, fc_infomask,
								fc_relation, &fc_oldtup.t_self, XLTW_Update,
								&fc_remain);
				fc_checked_lockers = true;
				fc_locker_remains = fc_remain != 0;
				LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

				/*
				 * 如果 xwait 刚锁定了元组，那么在我们到达这一点之前，
				 * 一些其他事务可能会更新这个元组。检查 xmax 更改，
				 * 如果是这样就重新开始。
				 */
				if (fc_xmax_infomask_changed(fc_oldtup.t_data->t_infomask,
										  fc_infomask) ||
					!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_oldtup.t_data),
										 fc_xwait))
					goto l2;
			}

			/*
			 * 注意，多重事务可能还没有完成。它可能有存活的成员；我们自己的事务或这个后端的其他子事务，以及任何其他并发事务，如果我们仅获得了LockTupleNoKeyExclusive，而锁定了这个元组的LockTupleKeyShare。 如果是这种情况，我们必须小心地在Xmax中标记具有存活成员的更新元组。
			 *
			 * 注意，MultiXact中可能有另一更新。在这种情况下，我们需要检查它是提交还是中止。如果它中止了，我们可以安全地再次更新；否则会出现更新冲突，我们必须如下面的TableTuple{Deleted, Updated}返回。
			 *
			 * 在LockTupleExclusive的情况下，我们仍然需要保留存活的成员：这包括我们在此之前拥有的元组锁，这在此子事务中止的情况下非常重要。
			 */
			if (!HEAP_XMAX_IS_LOCKED_ONLY(fc_oldtup.t_data->t_infomask))
				fc_update_xact = HeapTupleGetUpdateXid(fc_oldtup.t_data);
			else
				fc_update_xact = InvalidTransactionId;

			/*
			 * 在MultiXact中没有UPDATE；或者它已中止。这里不需要TransactionIdIsInProgress()调用，因为我们在上面调用了MultiXactIdWait()。
			 */
			if (!TransactionIdIsValid(fc_update_xact) ||
				TransactionIdDidAbort(fc_update_xact))
				fc_can_continue = true;
		}
		else if (TransactionIdIsCurrentTransactionId(fc_xwait))
		{
			/*
			 * 唯一的锁定者是我们自己；我们可以避免在这里获取元组锁，但必须保留我们的锁定信息。
			 */
			fc_checked_lockers = true;
			fc_locker_remains = true;
			fc_can_continue = true;
		}
		else if (HEAP_XMAX_IS_KEYSHR_LOCKED(fc_infomask) && fc_key_intact)
		{
			/*
			 * 如果它只是一个键共享锁定者，并且我们没有改变键列，我们不需要等待它结束；但我们需要将其保留为锁定者。
			 */
			fc_checked_lockers = true;
			fc_locker_remains = true;
			fc_can_continue = true;
		}
		else
		{
			/*
			 * 等待常规事务结束；但首先，获取元组锁。
			 */
			LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			fc_heap_acquire_tuplock(fc_relation, &(fc_oldtup.t_self), *fc_lockmode,
								 LockWaitBlock, &fc_have_tuple_lock);
			XactLockTableWait(fc_xwait, fc_relation, &fc_oldtup.t_self,
							  XLTW_Update);
			fc_checked_lockers = true;
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

			/*
			 * xwait已经完成，但如果xwait刚刚锁定了元组，那么在我们到达这个点之前，其他事务可能会更新这个元组。检查xmax是否改变，如果是这样，重新开始。
			 */
			if (fc_xmax_infomask_changed(fc_oldtup.t_data->t_infomask, fc_infomask) ||
				!TransactionIdEquals(fc_xwait,
									 HeapTupleHeaderGetRawXmax(fc_oldtup.t_data)))
				goto l2;

			/* 否则检查它是提交还是中止 */
			fc_UpdateXmaxHintBits(fc_oldtup.t_data, fc_buffer, fc_xwait);
			if (fc_oldtup.t_data->t_infomask & HEAP_XMAX_INVALID)
				fc_can_continue = true;
		}

		if (fc_can_continue)
			fc_result = TM_Ok;
		else if (!ItemPointerEquals(&fc_oldtup.t_self, &fc_oldtup.t_data->t_ctid))
			fc_result = TM_Updated;
		else
			fc_result = TM_Deleted;
	}

	/* 对HeapTupleSatisfiesUpdate()的结果和上述逻辑进行合理性检查 */
	if (fc_result != TM_Ok)
	{
		Assert(fc_result == TM_SelfModified ||
			   fc_result == TM_Updated ||
			   fc_result == TM_Deleted ||
			   fc_result == TM_BeingModified);
		Assert(!(fc_oldtup.t_data->t_infomask & HEAP_XMAX_INVALID));
		Assert(fc_result != TM_Updated ||
			   !ItemPointerEquals(&fc_oldtup.t_self, &fc_oldtup.t_data->t_ctid));
	}

	if (fc_crosscheck != InvalidSnapshot && fc_result == TM_Ok)
	{
		/* 对事务快照模式 RI 更新进行额外检查 */
		if (!HeapTupleSatisfiesVisibility(&fc_oldtup, fc_crosscheck, fc_buffer))
			fc_result = TM_Updated;
	}

	if (fc_result != TM_Ok)
	{
		fc_tmfd->ctid = fc_oldtup.t_data->t_ctid;
		fc_tmfd->xmax = HeapTupleHeaderGetUpdateXid(fc_oldtup.t_data);
		if (fc_result == TM_SelfModified)
			fc_tmfd->cmax = HeapTupleHeaderGetCmax(fc_oldtup.t_data);
		else
			fc_tmfd->cmax = InvalidCommandId;
		UnlockReleaseBuffer(fc_buffer);
		if (fc_have_tuple_lock)
			UnlockTupleTuplock(fc_relation, &(fc_oldtup.t_self), *fc_lockmode);
		if (fc_vmbuffer != InvalidBuffer)
			ReleaseBuffer(fc_vmbuffer);
		bms_free(fc_hot_attrs);
		bms_free(fc_key_attrs);
		bms_free(fc_id_attrs);
		bms_free(fc_modified_attrs);
		bms_free(fc_interesting_attrs);
		return fc_result;
	}

	/*
	 * 如果我们没有固定可见性映射页，并且在我们忙于锁定缓冲区时，页面变得完全可见，或者在一些后续窗口期间我们将其解锁，我们必须解锁并重新锁定，以避免在I/O期间持有缓冲区锁。 这有点不幸，特别是因为我们现在必须重新检查元组是否在我们之下被锁定或更新，但希望这种情况不会经常发生。
	 */
	if (fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page))
	{
		LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
		visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);
		LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
		goto l2;
	}

	/* 填充事务状态数据 */

	/*
	 * 如果我们正在更新的元组被锁定，我们需要在旧元组的Xmax中保留锁定信息。为此准备一个新的Xmax值。
	 */
	fc_compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(fc_oldtup.t_data),
							  fc_oldtup.t_data->t_infomask,
							  fc_oldtup.t_data->t_infomask2,
							  fc_xid, *fc_lockmode, true,
							  &fc_xmax_old_tuple, &fc_infomask_old_tuple,
							  &fc_infomask2_old_tuple);

	/*
	 * 还为新的元组副本准备一个Xmax值。如果之前没有xmax，或者有但所有锁定者现在都已离开，
	 * 那么使用InvalidTransactionId；否则，从旧的元组中获取xmax。 （在少数情况下，这也可能是InvalidTransactionId，但
	 * 仍然没有设置HEAP_XMAX_INVALID位；这没问题。）
	 */
	if ((fc_oldtup.t_data->t_infomask & HEAP_XMAX_INVALID) ||
		HEAP_LOCKED_UPGRADED(fc_oldtup.t_data->t_infomask) ||
		(fc_checked_lockers && !fc_locker_remains))
		fc_xmax_new_tuple = InvalidTransactionId;
	else
		fc_xmax_new_tuple = HeapTupleHeaderGetRawXmax(fc_oldtup.t_data);

	if (!TransactionIdIsValid(fc_xmax_new_tuple))
	{
		fc_infomask_new_tuple = HEAP_XMAX_INVALID;
		fc_infomask2_new_tuple = 0;
	}
	else
	{
		/*
		 * 如果我们为新的元组找到了有效的Xmax，那么新的元组所使用的infomask位取决于旧元组上所存在的位。
		 * 注意，由于我们正在进行更新，唯一的可能性是锁定者具有FOR KEY SHARE锁。
		 */
		if (fc_oldtup.t_data->t_infomask & HEAP_XMAX_IS_MULTI)
		{
			fc_GetMultiXactIdHintBits(fc_xmax_new_tuple, &fc_infomask_new_tuple,
								   &fc_infomask2_new_tuple);
		}
		else
		{
			fc_infomask_new_tuple = HEAP_XMAX_KEYSHR_LOCK | HEAP_XMAX_LOCK_ONLY;
			fc_infomask2_new_tuple = 0;
		}
	}

	/*
	 * 使用适当的Xmin和Xmax初始值，以及上述计算的初始infomask位，准备新的元组。
	 */
	fc_newtup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
	fc_newtup->t_data->t_infomask2 &= ~(HEAP2_XACT_MASK);
	HeapTupleHeaderSetXmin(fc_newtup->t_data, fc_xid);
	HeapTupleHeaderSetCmin(fc_newtup->t_data, fc_cid);
	fc_newtup->t_data->t_infomask |= HEAP_UPDATED | fc_infomask_new_tuple;
	fc_newtup->t_data->t_infomask2 |= fc_infomask2_new_tuple;
	HeapTupleHeaderSetXmax(fc_newtup->t_data, fc_xmax_new_tuple);

	/*
	 * 如果需要，使用组合CID替换cid。 注意我们已经将普通cid放入新的元组中。
	 */
	HeapTupleHeaderAdjustCmax(fc_oldtup.t_data, &fc_cid, &fc_iscombo);

	/*
	 * 如果需要激活toaster，或者新的元组无法与旧元组放在同一个页面上，那么我们需要释放旧元组缓冲区上的内容锁定
	 * （但不释放固定锁！）同时我们去进行TOAST和/或表文件扩展工作。我们必须标记旧元组，
	 * 以表明它已被锁定，否则其他进程可能会尝试自行更新它。
	 *
	 * 如果已经存在任何行外的toasted值，或者新的元组超过阈值，我们需要调用toaster。
	 */
	if (fc_relation->rd_rel->relkind != RELKIND_RELATION &&
		fc_relation->rd_rel->relkind != RELKIND_MATVIEW)
	{
		/* toast 表条目永远不应递归 toast */
		Assert(!HeapTupleHasExternal(&fc_oldtup));
		Assert(!HeapTupleHasExternal(fc_newtup));
		fc_need_toast = false;
	}
	else
		fc_need_toast = (HeapTupleHasExternal(&fc_oldtup) ||
					  HeapTupleHasExternal(fc_newtup) ||
					  fc_newtup->t_len > TOAST_TUPLE_THRESHOLD);

	fc_pagefree = PageGetHeapFreeSpace(fc_page);

	fc_newtupsize = MAXALIGN(fc_newtup->t_len);

	if (fc_need_toast || fc_newtupsize > fc_pagefree)
	{
		TransactionId fc_xmax_lock_old_tuple;
		uint16		fc_infomask_lock_old_tuple,
					fc_infomask2_lock_old_tuple;
		bool		fc_cleared_all_frozen = false;

		/*
		 * 为了防止并发会话更新元组，我们必须暂时将其标记为锁定，同时释放页面级锁。
		 *
		 * 为了满足任何可能出现在写入磁盘的缓冲区中的xid规则，不幸的是我们必须对这个临时修改进行WAL日志记录。
		 * 我们可以为此目的重用xl_heap_lock。如果我们在进行实际更新之前崩溃/出错，xmax将是一个已中止的事务，
		 * 允许其他会话进行。
		 */

		/*
		 * 计算适合锁定元组的xmax / infomask。 这必须与将用于更新的组合单独进行，因为
		 * 否则可能创建的multixact将是错误的。
		 */
		fc_compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(fc_oldtup.t_data),
								  fc_oldtup.t_data->t_infomask,
								  fc_oldtup.t_data->t_infomask2,
								  fc_xid, *fc_lockmode, false,
								  &fc_xmax_lock_old_tuple, &fc_infomask_lock_old_tuple,
								  &fc_infomask2_lock_old_tuple);

		Assert(HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask_lock_old_tuple));

		START_CRIT_SECTION();

		/* 清除过时的可见性标志... */
		fc_oldtup.t_data->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
		fc_oldtup.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		HeapTupleClearHotUpdated(&fc_oldtup);
		/* ... 并存储关于更新此元组的事务的信息 */
		Assert(TransactionIdIsValid(fc_xmax_lock_old_tuple));
		HeapTupleHeaderSetXmax(fc_oldtup.t_data, fc_xmax_lock_old_tuple);
		fc_oldtup.t_data->t_infomask |= fc_infomask_lock_old_tuple;
		fc_oldtup.t_data->t_infomask2 |= fc_infomask2_lock_old_tuple;
		HeapTupleHeaderSetCmax(fc_oldtup.t_data, fc_cid, fc_iscombo);

		/* 暂时使其看起来未更新，但已被锁定 */
		fc_oldtup.t_data->t_ctid = fc_oldtup.t_self;

		/*
		 * 如有必要，清除可见性图上的所有冻结位。我们可以立即重置ALL_VISIBLE，
		 * 但鉴于WAL日志记录的开销保持不变，这似乎不是必需的。
		 */
		if (PageIsAllVisible(fc_page) &&
			visibilitymap_clear(fc_relation, fc_block, fc_vmbuffer,
								VISIBILITYMAP_ALL_FROZEN))
			fc_cleared_all_frozen = true;

		MarkBufferDirty(fc_buffer);

		if (RelationNeedsWAL(fc_relation))
		{
			xl_heap_lock fc_xlrec;
			XLogRecPtr	fc_recptr;

			XLogBeginInsert();
			XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);

			fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_oldtup.t_self);
			fc_xlrec.locking_xid = fc_xmax_lock_old_tuple;
			fc_xlrec.infobits_set = fc_compute_infobits(fc_oldtup.t_data->t_infomask,
												  fc_oldtup.t_data->t_infomask2);
			fc_xlrec.flags =
				fc_cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0;
			XLogRegisterData((char *) &fc_xlrec, SizeOfHeapLock);
			fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_LOCK);
			PageSetLSN(fc_page, fc_recptr);
		}

		END_CRIT_SECTION();

		LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

		/*
		 * 让toaster做它的事情，如果需要的话。
		 *
		 * 注意：在这一点以下，heaptup是我们实际上打算存储到关系中的数据；newtup是调用者的原始未toasted数据。
		 */
		if (fc_need_toast)
		{
			/* 注意，我们在更新期间始终使用WAL和FSM */
			fc_heaptup = heap_toast_insert_or_update(fc_relation, fc_newtup, &fc_oldtup, 0);
			fc_newtupsize = MAXALIGN(fc_heaptup->t_len);
		}
		else
			fc_heaptup = fc_newtup;

		/*
		 * 现在，我们需要为元组分配一个新页面吗？这有点棘手，因为在我们不注意的时候，可能有其他人向页面添加了元组。我们必须在重新获取缓冲区锁后重新检查可用空间。但如果之前的空闲空间仍然不够，那就不用再检查了；现在的空闲空间比之前多的可能性不大。
		 *
		 * 更重要的是，如果需要获取一个新页面，我们将需要在旧页面和新页面上获取缓冲区锁。为了避免在尝试以其他顺序获取同两个锁时与其他后端发生死锁，我们必须对获取锁的顺序保持一致。我们使用“先锁定关系的低编号页面”的规则。要实现这一点，我们必须在没有持有旧页面锁的情况下执行RelationGetBufferForTuple，并且我们必须依赖它以正确的顺序获取两个页面的锁。
		 *
		 * 另一个考虑因素是，如果我们必须清除任一页面上的all-visible标志，我们需要可见性图页面锁。如果我们调用RelationGetBufferForTuple，我们依赖它来获取任何这样的锁；但如果不调用，我们必须在这里处理。所以我们需要一个循环。
		 */
		for (;;)
		{
			if (fc_newtupsize > fc_pagefree)
			{
				/* 不适合，必须使用RelationGetBufferForTuple。 */
				fc_newbuf = RelationGetBufferForTuple(fc_relation, fc_heaptup->t_len,
												   fc_buffer, 0, NULL,
												   &fc_vmbuffer_new, &fc_vmbuffer);
				/* 我们都完成了。 */
				break;
			}
			/* 如果需要且我们没有锁定，获取VM页面锁。 */
			if (fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page))
				visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);
			/* 重新获取旧元组页面的锁。 */
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
			/* 使用最新的空闲空间重新检查 */
			fc_pagefree = PageGetHeapFreeSpace(fc_page);
			if (fc_newtupsize > fc_pagefree ||
				(fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page)))
			{
				/*
				 * 糟糕，不再适合，或者有人刚刚设置了
				 * all-visible标志。我们现在必须解锁并循环以避免
				 * 死锁。幸运的是，这条路径应该很少被采用。
				 */
				LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			}
			else
			{
				/* 我们都完成了。 */
				fc_newbuf = fc_buffer;
				break;
			}
		}
	}
	else
	{
		/* 不需要TOAST工作，并且可以适应同一页面 */
		fc_newbuf = fc_buffer;
		fc_heaptup = fc_newtup;
	}

	/*
	 * 我们即将进行实际更新 -- 首先检查冲突，以避免可能需要回滚我们刚刚完成的工作。
	 *
	 * 只要没有其他进程在此检查和更新可见之间扫描页面的可能性，这样做是安全的（即，独占缓冲区内容锁从此时到元组更新可见时持续保持）。
	 *
	 * 对于新元组，只需要在关系级别进行检查，但由于两个元组在同一个关系中，且对于oldtup的检查将包括检查关系级别，因此对新元组的单独检查没有好处。
	 */
	CheckForSerializableConflictIn(fc_relation, &fc_oldtup.t_self,
								   BufferGetBlockNumber(fc_buffer));

	/*
	 * 此时newbuf和buffer都已被锁定，并且newbuf
	 * 有足够的空间来存放新的元组。如果它们是同一个缓冲区，则只持有一个锁。
	 */

	if (fc_newbuf == fc_buffer)
	{
		/*
		 * 由于新元组将放入同一页面，我们可能能够
		 * 进行HOT更新。检查是否有任何索引列已被
		 * 修改。
		 */
		if (!bms_overlap(fc_modified_attrs, fc_hot_attrs))
			fc_use_hot_update = true;
	}
	else
	{
		/* 设置一个提示，表明旧页面可以进行修剪/碎片整理 */
		PageSetFull(fc_page);
	}

	/*
	 * 在进入关键区之前计算副本身份元组，以便在内存分配失败时
	 * 不会PANIC。
	 * ExtractReplicaIdentity() 如果没有需要记录的内容将返回 NULL。
	 * 如果副本身份键列被修改或有外部数据，传递旧键时需要为真。
	 */
	fc_old_key_tuple = fc_ExtractReplicaIdentity(fc_relation, &fc_oldtup,
										   bms_overlap(fc_modified_attrs, fc_id_attrs) ||
										   fc_id_has_external,
										   &fc_old_key_copied);

	/* 从这里到更改记录时不报告错误 */
	START_CRIT_SECTION();

	/*
	 * 如果这个事务提交，旧元组迟早会变成 DEAD。
	 * 设置标志，表示一旦我们的 xid 低于 OldestXmin 水平，该页面
	 * 是修剪的候选者。如果事务最终中止，后续的页面修剪将是无操作，
	 * 提示将被清除。
	 *
	 * XXX 我们是否也应该在 newbuf 上设置提示？如果事务中止，
	 * newbuf 中会有一个可以修剪的元组；但现在我们选择不为中止做优化。
	 * 注意，如果这个决定改变，heap_xlog_update 必须保持同步。
	 */
	PageSetPrunable(fc_page, fc_xid);

	if (fc_use_hot_update)
	{
		/* 将旧元组标记为 HOT 更新 */
		HeapTupleSetHotUpdated(&fc_oldtup);
		/* 并将新元组标记为仅堆 */
		HeapTupleSetHeapOnly(fc_heaptup);
		/* 也标记调用者的副本，以防与 heaptup 不同 */
		HeapTupleSetHeapOnly(fc_newtup);
	}
	else
	{
		/* 确保元组正确标记为非 HOT */
		HeapTupleClearHotUpdated(&fc_oldtup);
		HeapTupleClearHeapOnly(fc_heaptup);
		HeapTupleClearHeapOnly(fc_newtup);
	}

	RelationPutHeapTuple(fc_relation, fc_newbuf, fc_heaptup, false); /* 插入新元组 */


	/* 清除过时的可见性标志，可能由我们自己在上面设置... */
	fc_oldtup.t_data->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
	fc_oldtup.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED;
	/* ... 并存储关于更新此元组的事务的信息 */
	Assert(TransactionIdIsValid(fc_xmax_old_tuple));
	HeapTupleHeaderSetXmax(fc_oldtup.t_data, fc_xmax_old_tuple);
	fc_oldtup.t_data->t_infomask |= fc_infomask_old_tuple;
	fc_oldtup.t_data->t_infomask2 |= fc_infomask2_old_tuple;
	HeapTupleHeaderSetCmax(fc_oldtup.t_data, fc_cid, fc_iscombo);

	/* 在旧元组的 t_ctid 中记录新元组的地址 */
	fc_oldtup.t_data->t_ctid = fc_heaptup->t_self;

	/* 清除 PD_ALL_VISIBLE 标志，重置所有可见性映射位 */
	if (PageIsAllVisible(BufferGetPage(fc_buffer)))
	{
		fc_all_visible_cleared = true;
		PageClearAllVisible(BufferGetPage(fc_buffer));
		visibilitymap_clear(fc_relation, BufferGetBlockNumber(fc_buffer),
							fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
	}
	if (fc_newbuf != fc_buffer && PageIsAllVisible(BufferGetPage(fc_newbuf)))
	{
		fc_all_visible_cleared_new = true;
		PageClearAllVisible(BufferGetPage(fc_newbuf));
		visibilitymap_clear(fc_relation, BufferGetBlockNumber(fc_newbuf),
							fc_vmbuffer_new, VISIBILITYMAP_VALID_BITS);
	}

	if (fc_newbuf != fc_buffer)
		MarkBufferDirty(fc_newbuf);
	MarkBufferDirty(fc_buffer);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_relation))
	{
		XLogRecPtr	fc_recptr;

		/*
		 * 对于逻辑解码，我们需要组合 CID 以正确解码目录。
		 */
		if (RelationIsAccessibleInLogicalDecoding(fc_relation))
		{
			fc_log_heap_new_cid(fc_relation, &fc_oldtup);
			fc_log_heap_new_cid(fc_relation, fc_heaptup);
		}

		fc_recptr = fc_log_heap_update(fc_relation, fc_buffer,
								 fc_newbuf, &fc_oldtup, fc_heaptup,
								 fc_old_key_tuple,
								 fc_all_visible_cleared,
								 fc_all_visible_cleared_new);
		if (fc_newbuf != fc_buffer)
		{
			PageSetLSN(BufferGetPage(fc_newbuf), fc_recptr);
		}
		PageSetLSN(BufferGetPage(fc_buffer), fc_recptr);
	}

	END_CRIT_SECTION();

	if (fc_newbuf != fc_buffer)
		LockBuffer(fc_newbuf, BUFFER_LOCK_UNLOCK);
	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

	/*
	 * 在下一个命令边界处将旧元组标记为从系统缓存失效，并在
	 * 我们中止的情况下将新元组标记为失效。我们必须在释放
	 * 缓冲区之前执行此操作，因为 oldtup 在缓冲区中。
	 * （heaptup 都在本地内存中，但处理两个元组版本的请求
	 * 是必要的，以便我们能避免冗余的 sinval 消息。）
	 */
	CacheInvalidateHeapTuple(fc_relation, &fc_oldtup, fc_heaptup);

	/* 现在我们可以释放缓冲区 */
	if (fc_newbuf != fc_buffer)
		ReleaseBuffer(fc_newbuf);
	ReleaseBuffer(fc_buffer);
	if (BufferIsValid(fc_vmbuffer_new))
		ReleaseBuffer(fc_vmbuffer_new);
	if (BufferIsValid(fc_vmbuffer))
		ReleaseBuffer(fc_vmbuffer);

	/*
	 * 释放lmgr元组锁，如果我们有的话。
	 */
	if (fc_have_tuple_lock)
		UnlockTupleTuplock(fc_relation, &(fc_oldtup.t_self), *fc_lockmode);

	pgstat_count_heap_update(fc_relation, fc_use_hot_update);

	/*
	 * 如果 heaptup 是私有副本，则释放它。 不要忘记将 t_self 复制回调用者的图像中。
	 */
	if (fc_heaptup != fc_newtup)
	{
		fc_newtup->t_self = fc_heaptup->t_self;
		heap_freetuple(fc_heaptup);
	}

	if (fc_old_key_tuple != NULL && fc_old_key_copied)
		heap_freetuple(fc_old_key_tuple);

	bms_free(fc_hot_attrs);
	bms_free(fc_key_attrs);
	bms_free(fc_id_attrs);
	bms_free(fc_modified_attrs);
	bms_free(fc_interesting_attrs);

	return TM_Ok;
}

#ifdef USE_ASSERT_CHECKING
/*
 * 根据 README.tuplock 部分“锁定以写入就地更新表”的规则确认在 heap_update()
 * 期间持有足够的锁。
 */
static void fc_check_lock_if_inplace_updateable_rel(Relation fc_relation,
									 ItemPointer fc_otid,
									 HeapTuple fc_newtup)
{
	/* LOCKTAG_TUPLE 对于任何目录都是可接受的 */
	switch (RelationGetRelid(fc_relation))
	{
		case RelationRelationId:
		case DatabaseRelationId:
			{
				LOCKTAG		fc_tuptag;

				SET_LOCKTAG_TUPLE(fc_tuptag,
								  fc_relation->rd_lockInfo.lockRelId.dbId,
								  fc_relation->rd_lockInfo.lockRelId.relId,
								  ItemPointerGetBlockNumber(fc_otid),
								  ItemPointerGetOffsetNumber(fc_otid));
				if (LockHeldByMe(&fc_tuptag, InplaceUpdateTupleLock))
					return;
			}
			break;
		default:
			Assert(!IsInplaceUpdateRelation(fc_relation));
			return;
	}

	switch (RelationGetRelid(fc_relation))
	{
		case RelationRelationId:
			{
				/* LOCKTAG_TUPLE 或 LOCKTAG_RELATION 可以 */
				Form_pg_class fc_classForm = (Form_pg_class) GETSTRUCT(fc_newtup);
				Oid			fc_relid = fc_classForm->oid;
				Oid			fc_dbid;
				LOCKTAG		fc_tag;

				if (IsSharedRelation(fc_relid))
					fc_dbid = InvalidOid;
				else
					fc_dbid = MyDatabaseId;

				if (fc_classForm->relkind == RELKIND_INDEX)
				{
					Relation	fc_irel = index_open(fc_relid, AccessShareLock);

					SET_LOCKTAG_RELATION(fc_tag, fc_dbid, fc_irel->rd_index->indrelid);
					index_close(fc_irel, AccessShareLock);
				}
				else
					SET_LOCKTAG_RELATION(fc_tag, fc_dbid, fc_relid);

				if (!LockHeldByMe(&fc_tag, ShareUpdateExclusiveLock) &&
					!LockOrStrongerHeldByMe(&fc_tag, ShareRowExclusiveLock))
					elog(WARNING,
						 "missing lock for relation \"%s\" (OID %u, relkind %c) @ TID (%u,%u)",
						 NameStr(fc_classForm->relname),
						 fc_relid,
						 fc_classForm->relkind,
						 ItemPointerGetBlockNumber(fc_otid),
						 ItemPointerGetOffsetNumber(fc_otid));
			}
			break;
		case DatabaseRelationId:
			{
				/* 需要 LOCKTAG_TUPLE */
				Form_pg_database fc_dbForm = (Form_pg_database) GETSTRUCT(fc_newtup);

				elog(WARNING,
					 "missing lock on database \"%s\" (OID %u) @ TID (%u,%u)",
					 NameStr(fc_dbForm->datname),
					 fc_dbForm->oid,
					 ItemPointerGetBlockNumber(fc_otid),
					 ItemPointerGetOffsetNumber(fc_otid));
			}
			break;
	}
}

/*
 * 根据 README.tuplock 部分“锁定以写入就地更新表”的规则确认已持有足够的关系锁。
 */
static void fc_check_inplace_rel_lock(HeapTuple fc_oldtup)
{
	Form_pg_class fc_classForm = (Form_pg_class) GETSTRUCT(fc_oldtup);
	Oid			fc_relid = fc_classForm->oid;
	Oid			fc_dbid;
	LOCKTAG		fc_tag;

	if (IsSharedRelation(fc_relid))
		fc_dbid = InvalidOid;
	else
		fc_dbid = MyDatabaseId;

	if (fc_classForm->relkind == RELKIND_INDEX)
	{
		Relation	fc_irel = index_open(fc_relid, AccessShareLock);

		SET_LOCKTAG_RELATION(fc_tag, fc_dbid, fc_irel->rd_index->indrelid);
		index_close(fc_irel, AccessShareLock);
	}
	else
		SET_LOCKTAG_RELATION(fc_tag, fc_dbid, fc_relid);

	if (!LockOrStrongerHeldByMe(&fc_tag, ShareUpdateExclusiveLock))
		elog(WARNING,
			 "missing lock for relation \"%s\" (OID %u, relkind %c) @ TID (%u,%u)",
			 NameStr(fc_classForm->relname),
			 fc_relid,
			 fc_classForm->relkind,
			 ItemPointerGetBlockNumber(&fc_oldtup->t_self),
			 ItemPointerGetOffsetNumber(&fc_oldtup->t_self));
}
#endif

/*
 * 检查指定属性的值是否相同。HeapDetermineColumnsInfo 的子程序。
 */
static bool fc_heap_attr_equals(TupleDesc fc_tupdesc, int fc_attrnum, Datum fc_value1, Datum fc_value2,
				 bool fc_isnull1, bool fc_isnull2)
{
	Form_pg_attribute fc_att;

	/*
	 * 如果一个值是 NULL，另一个不是，那么它们肯定不相等
	 */
	if (fc_isnull1 != fc_isnull2)
		return false;

	/*
	 * 如果两个都是 NULL，则可以认为它们相等。
	 */
	if (fc_isnull1)
		return true;

	/*
	 * 我们对两个数据项进行简单的二进制比较。这可能过于严格，因为同一逻辑值可能有多种二进制表示。但只要没有误报，我们应该没问题。使用特定类型的相等运算符比较麻烦，因为在不同的运算符类中可能有多种相等的概念；此外，在持有独占缓冲锁时，我们不能安全调用用户定义的函数。
	 */
	if (fc_attrnum <= 0)
	{
		/* 唯一允许的系统列是 OID，因此执行此操作 */
		return (DatumGetObjectId(fc_value1) == DatumGetObjectId(fc_value2));
	}
	else
	{
		Assert(fc_attrnum <= fc_tupdesc->natts);
		fc_att = TupleDescAttr(fc_tupdesc, fc_attrnum - 1);
		return datumIsEqual(fc_value1, fc_value2, fc_att->attbyval, fc_att->attlen);
	}
}

/*
 * 检查哪些列正在被更新。
 *
 * 给定一个更新的元组，确定（并返回到输出 bitmapset 中），
 * 从那些列中找出发生更改的列集合。
 *
 * has_external 表示旧元组中未修改属性（从那些列中）是否为 external_cols 的成员且是否存储在外部。
 *
 * 输入的 interesting_cols bitmapset 被破坏性修改；这是可以的，因为这个过程在 heap_update 中最多被调用一次。
 */
static Bitmapset * fc_HeapDetermineColumnsInfo(Relation fc_relation,
						 Bitmapset *fc_interesting_cols,
						 Bitmapset *fc_external_cols,
						 HeapTuple fc_oldtup, HeapTuple fc_newtup,
						 bool *fc_has_external)
{
	int			fc_attrnum;
	Bitmapset  *fc_modified = NULL;
	TupleDesc	fc_tupdesc = RelationGetDescr(fc_relation);

	while ((fc_attrnum = bms_first_member(fc_interesting_cols)) >= 0)
	{
		Datum		fc_value1,
					fc_value2;
		bool		fc_isnull1,
					fc_isnull2;

		fc_attrnum += FirstLowInvalidHeapAttributeNumber;

		/*
		 * 如果是整个元组的引用，返回“不同”。实际上不值得支持这种情况，因为它只能在无操作更新后成功，而这不是值得优化的情况。
		 */
		if (fc_attrnum == 0)
		{
			fc_modified = bms_add_member(fc_modified,
									  fc_attrnum -
									  FirstLowInvalidHeapAttributeNumber);
			continue;
		}

		/*
		 * 同样，对于除 tableOID 之外的任何系统属性，自动返回“不同”；我们不能期望这些在 HOT 链中是一致的，甚至在新元组中也可能未正确设置。
		 */
		if (fc_attrnum < 0)
		{
			if (fc_attrnum != TableOidAttributeNumber)
			{
				fc_modified = bms_add_member(fc_modified,
										  fc_attrnum -
										  FirstLowInvalidHeapAttributeNumber);
				continue;
			}
		}

		/*
		 * 提取相应的值。XXX 如果有许多索引列，这样效率相当低下。我们是否应该对每个元组做一次单独的 heap_deform_tuple 调用？但这对系统列不适用...
		 */
		fc_value1 = heap_getattr(fc_oldtup, fc_attrnum, fc_tupdesc, &fc_isnull1);
		fc_value2 = heap_getattr(fc_newtup, fc_attrnum, fc_tupdesc, &fc_isnull2);

		if (!fc_heap_attr_equals(fc_tupdesc, fc_attrnum, fc_value1,
							  fc_value2, fc_isnull1, fc_isnull2))
		{
			fc_modified = bms_add_member(fc_modified,
									  fc_attrnum -
									  FirstLowInvalidHeapAttributeNumber);
			continue;
		}

		/*
		 * 无需检查无法存储在外部的属性。请注意，系统属性无法存储在外部。
		 */
		if (fc_attrnum < 0 || fc_isnull1 ||
			TupleDescAttr(fc_tupdesc, fc_attrnum - 1)->attlen != -1)
			continue;

		/*
		 * 检查旧元组的属性是否存储在外部且是 external_cols 的成员。
		 */
		if (VARATT_IS_EXTERNAL((struct varlena *) DatumGetPointer(fc_value1)) &&
			bms_is_member(fc_attrnum - FirstLowInvalidHeapAttributeNumber,
						  fc_external_cols))
			*fc_has_external = true;
	}

	return fc_modified;
}

/*
 *	simple_heap_update - 替换一个元组
 *
 * 当不期望对目标元组进行并发更新时（例如，因为我们对与元组相关的关系有锁），
 * 可以使用此例程来更新元组。任何失败将通过 ereport() 报告。
 */
void simple_heap_update(Relation fc_relation, ItemPointer fc_otid, HeapTuple fc_tup)
{
	TM_Result	fc_result;
	TM_FailureData fc_tmfd;
	LockTupleMode fc_lockmode;

	fc_result = heap_update(fc_relation, fc_otid, fc_tup,
						 GetCurrentCommandId(true), InvalidSnapshot,
						 true /* 等待提交 */ ,
						 &fc_tmfd, &fc_lockmode);
	switch (fc_result)
	{
		case TM_SelfModified:
			/* 元组在当前命令中已经被更新？ */
			elog(ERROR, "tuple already updated by self");
			break;

		case TM_Ok:
			/* 成功完成 */
			break;

		case TM_Updated:
			elog(ERROR, "tuple concurrently updated");
			break;

		case TM_Deleted:
			elog(ERROR, "tuple concurrently deleted");
			break;

		default:
			elog(ERROR, "unrecognized heap_update status: %u", fc_result);
			break;
	}
}


/*
 * 返回与给定元组锁模式对应的 MultiXactStatus。
 */
static MultiXactStatus
fc_get_mxact_status_for_lock(LockTupleMode fc_mode, bool fc_is_update)
{
	int			fc_retval;

	if (fc_is_update)
		fc_retval = tupleLockExtraInfo[fc_mode].updstatus;
	else
		fc_retval = tupleLockExtraInfo[fc_mode].lockstatus;

	if (fc_retval == -1)
		elog(ERROR, "invalid lock tuple mode %d/%s", fc_mode,
			 fc_is_update ? "true" : "false");

	return (MultiXactStatus) fc_retval;
}

/*
 *	heap_lock_tuple - 在共享或独占模式下锁定一个元组
 *
 * 注意，这会获取一个缓冲区引脚，调用者必须释放它。
 *
 * 输入参数：
 *	关系：包含元组的关系（调用者必须保持适当的锁）
 *	tid：要锁定的元组的TID
 *	cid：当前命令ID（用于可见性测试，如果锁定成功则存储到
 *		元组的cmax中）
 *	mode：指示是否需要共享或独占元组锁
 *	wait_policy：如果元组锁不可用，则该如何处理
 *	follow_updates：如果为真，则跟随更新链也锁定后代
 *		元组。
 *
 * 输出参数：
 *	*tuple：所有字段已填充
 *	*buffer：设置为持有元组的缓冲区（退出时已固定但未锁定）
 *	*tmfd：在失败情况下填充（见下文）
 *
 * 函数结果与table_tuple_lock()的结果相同。
 *
 * 在除TM_Invisible之外的失败案例中，例程填充
 * *tmfd与元组的t_ctid、t_xmax（如有必要解析可能的MultiXact）和t_cmax（最后仅对TM_SelfModified，
 * 因为我们无法从另一个事务生成的组合CID中获得cmax）。
 * 有关其他信息，请参阅struct TM_FailureData的注释。
 *
 * 请参阅README.tuplock以获取该机制的详细说明。
 */
TM_Result heap_lock_tuple(Relation fc_relation, HeapTuple fc_tuple,
				CommandId fc_cid, LockTupleMode fc_mode, LockWaitPolicy fc_wait_policy,
				bool fc_follow_updates,
				Buffer *fc_buffer, TM_FailureData *fc_tmfd)
{
	TM_Result	fc_result;
	ItemPointer fc_tid = &(fc_tuple->t_self);
	ItemId		fc_lp;
	Page		fc_page;
	Buffer		fc_vmbuffer = InvalidBuffer;
	BlockNumber fc_block;
	TransactionId fc_xid,
				fc_xmax;
	uint16		fc_old_infomask,
				fc_new_infomask,
				fc_new_infomask2;
	bool		fc_first_time = true;
	bool		fc_skip_tuple_lock = false;
	bool		fc_have_tuple_lock = false;
	bool		fc_cleared_all_frozen = false;

	*fc_buffer = ReadBuffer(fc_relation, ItemPointerGetBlockNumber(fc_tid));
	fc_block = ItemPointerGetBlockNumber(fc_tid);

	/*
	 * 在锁定缓冲区之前，如果显得有必要，则固定可见性映射页面。
	 * 由于我们还没有获得锁，其他人可能正在更改它，因此在我们获得锁后需要重新检查。
	 */
	if (PageIsAllVisible(BufferGetPage(*fc_buffer)))
		visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);

	LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);

	fc_page = BufferGetPage(*fc_buffer);
	fc_lp = PageGetItemId(fc_page, ItemPointerGetOffsetNumber(fc_tid));
	Assert(ItemIdIsNormal(fc_lp));

	fc_tuple->t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);
	fc_tuple->t_len = ItemIdGetLength(fc_lp);
	fc_tuple->t_tableOid = RelationGetRelid(fc_relation);

l3:
	fc_result = HeapTupleSatisfiesUpdate(fc_tuple, fc_cid, *fc_buffer);

	if (fc_result == TM_Invisible)
	{
		/*
		 * 这可能，但仅在锁定元组以进行ON CONFLICT
		 * UPDATE时。在此返回该值而不是抛出错误，以便为该情况提供
		 * 抛出更具体错误的机会。
		 */
		fc_result = TM_Invisible;
		goto out_locked;
	}
	else if (fc_result == TM_BeingModified ||
			 fc_result == TM_Updated ||
			 fc_result == TM_Deleted)
	{
		TransactionId fc_xwait;
		uint16		fc_infomask;
		uint16		fc_infomask2;
		bool		fc_require_sleep;
		ItemPointerData fc_t_ctid;

		/* 在解锁缓冲区之前必须复制状态数据 */
		fc_xwait = HeapTupleHeaderGetRawXmax(fc_tuple->t_data);
		fc_infomask = fc_tuple->t_data->t_infomask;
		fc_infomask2 = fc_tuple->t_data->t_infomask2;
		ItemPointerCopy(&fc_tuple->t_data->t_ctid, &fc_t_ctid);

		LockBuffer(*fc_buffer, BUFFER_LOCK_UNLOCK);

		/*
		 * 如果当前顶部事务的任何子事务已经持有
		 * 与我们请求的锁一样强或更强的锁，我们
		 * 实际上已经持有所需的锁。我们*必须*在不尝试获取元组锁的情况下成功，
		 * 否则我们将与任何想要获取更强锁的事务死锁。
		 *
		 * 注意，我们仅在第一次循环HTSU结果时这样做；
		 * 在后续传递中测试没有意义，因为显然
		 * 我们自己的事务在第一次检查后无法获得新锁。
		 */
		if (fc_first_time)
		{
			fc_first_time = false;

			if (fc_infomask & HEAP_XMAX_IS_MULTI)
			{
				int			fc_i;
				int			fc_nmembers;
				MultiXactMember *fc_members;

				/*
				 * 我们不需要在这里允许旧的多重事务；如果是那样，
				 * HeapTupleSatisfiesUpdate将返回
				 * MayBeUpdated，而我们不会在这里。
				 */
				fc_nmembers =
					GetMultiXactIdMembers(fc_xwait, &fc_members, false,
										  HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask));

				for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
				{
					/* 仅考虑我们自己事务的成员 */
					if (!TransactionIdIsCurrentTransactionId(fc_members[fc_i].xid))
						continue;

					if (TUPLOCK_from_mxstatus(fc_members[fc_i].status) >= fc_mode)
					{
						pfree(fc_members);
						fc_result = TM_Ok;
						goto out_unlocked;
					}
					else
					{
						/*
						 * 禁用对重量级元组锁的获取。
						 * 否则，在提升一个较弱的锁时，我们可能会
						 * 与另一个已经获取重量级元组锁并且正在
						 * 等待我们事务完成的锁定者发生死锁。
						 *
						 * 注意，在这种情况下，我们仍然需要等待
						 * multixact（多重事务），如果需要，
						 * 以避免获取冲突的锁。
						 */
						fc_skip_tuple_lock = true;
					}
				}

				if (fc_members)
					pfree(fc_members);
			}
			else if (TransactionIdIsCurrentTransactionId(fc_xwait))
			{
				switch (fc_mode)
				{
					case LockTupleKeyShare:
						Assert(HEAP_XMAX_IS_KEYSHR_LOCKED(fc_infomask) ||
							   HEAP_XMAX_IS_SHR_LOCKED(fc_infomask) ||
							   HEAP_XMAX_IS_EXCL_LOCKED(fc_infomask));
						fc_result = TM_Ok;
						goto out_unlocked;
					case LockTupleShare:
						if (HEAP_XMAX_IS_SHR_LOCKED(fc_infomask) ||
							HEAP_XMAX_IS_EXCL_LOCKED(fc_infomask))
						{
							fc_result = TM_Ok;
							goto out_unlocked;
						}
						break;
					case LockTupleNoKeyExclusive:
						if (HEAP_XMAX_IS_EXCL_LOCKED(fc_infomask))
						{
							fc_result = TM_Ok;
							goto out_unlocked;
						}
						break;
					case LockTupleExclusive:
						if (HEAP_XMAX_IS_EXCL_LOCKED(fc_infomask) &&
							fc_infomask2 & HEAP_KEYS_UPDATED)
						{
							fc_result = TM_Ok;
							goto out_unlocked;
						}
						break;
				}
			}
		}

		/*
		 * 初始假设我们必须等待锁定
		 * 事务完成。我们在下面检查可以
		 * 关闭此功能的各种情况。
		 */
		fc_require_sleep = true;
		if (fc_mode == LockTupleKeyShare)
		{
			/*
			 * 如果我们请求KeyShare，并且没有更新存在，我们
			 * 不需要等待。即使有更新，如果键没有被修改，
			 * 我们仍然可以继续。
			 *
			 * 然而，如果有更新，我们需要遍历更新链
			 * 来标记该行的未来版本为已锁定。这样，
			 * 如果有人删除了那个未来版本，我们可以防止
			 * 键的消失。这个未来版本的锁定
			 * 可能会稍微阻塞，如果一个并发事务正在
			 * 删除一个键；或者它可能会返回一个值，表示
			 * 删除键的事务已被提交。所以我们
			 * 在重新锁定缓冲区之前执行此操作；否则这可能
			 * 会导致死锁。
			 *
			 * 注意，我们锁定的TID是在我们解锁
			 * 缓冲区之前获取的。为了使其在我们不监视
			 * 时发生变化，我们在重新锁定
			 * 缓冲区后测试的其他属性也会变化，在这种情况下
			 * 我们将重新启动上面的循环。
			 */
			if (!(fc_infomask2 & HEAP_KEYS_UPDATED))
			{
				bool		fc_updated;

				fc_updated = !HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask);

				/*
				 * 如果有更新，遵循更新链；如果
				 * 无法这样做，则退出。
				 */
				if (fc_follow_updates && fc_updated)
				{
					TM_Result	fc_res;

					fc_res = fc_heap_lock_updated_tuple(fc_relation, fc_tuple, &fc_t_ctid,
												  GetCurrentTransactionId(),
												  fc_mode);
					if (fc_res != TM_Ok)
					{
						fc_result = fc_res;
						/* 恢复代码期望保持缓冲区锁 */
						LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
						goto failed;
					}
				}

				LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);

				/*
				 * 确保它仍然是适当的锁，否则重新开始。
				 * 此外，如果在我们释放锁之前没有被更新，
				 * 但现在被更新，我们也重新开始；原因是我们
				 * 现在需要遵循更新链来锁定新的
				 * 版本。
				 */
				if (!HeapTupleHeaderIsOnlyLocked(fc_tuple->t_data) &&
					((fc_tuple->t_data->t_infomask2 & HEAP_KEYS_UPDATED) ||
					 !fc_updated))
					goto l3;

				/* 一切看起来还好，所以我们可以跳过睡眠 */
				fc_require_sleep = false;

				/*
				 * 注意，我们允许Xmax在此处更改；其他更新者/锁定者
				 * 可能在我们获取缓冲区锁之前已经修改了它。
				 * 然而，这不是问题，因为通过我们刚刚进行的重检
				 * 我们确保它们仍然与我们想要的锁不冲突。
				 */
			}
		}
		else if (fc_mode == LockTupleShare)
		{
			/*
			 * 如果我们请求Share，如果没有更新且没有独占锁
			 * 存在，我们也可以避免睡眠。
			 */
			if (HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask) &&
				!HEAP_XMAX_IS_EXCL_LOCKED(fc_infomask))
			{
				LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);

				/*
				 * 确保它仍然是适当的锁，否则重新开始。
				 * 请参见上面关于允许xmax更改的内容。
				 */
				if (!HEAP_XMAX_IS_LOCKED_ONLY(fc_tuple->t_data->t_infomask) ||
					HEAP_XMAX_IS_EXCL_LOCKED(fc_tuple->t_data->t_infomask))
					goto l3;
				fc_require_sleep = false;
			}
		}
		else if (fc_mode == LockTupleNoKeyExclusive)
		{
			/*
			 * 如果我们请求NoKeyExclusive，我们还可能能够
			 * 避免睡眠；只需确保没有已获取的冲突锁。
			 */
			if (fc_infomask & HEAP_XMAX_IS_MULTI)
			{
				if (!fc_DoesMultiXactIdConflict((MultiXactId) fc_xwait, fc_infomask,
											 fc_mode, NULL))
				{
					/*
					 * 没有冲突，但如果在此期间 xmax 发生了变化，重新开始。
					 */
					LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
					if (fc_xmax_infomask_changed(fc_tuple->t_data->t_infomask, fc_infomask) ||
						!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tuple->t_data),
											 fc_xwait))
						goto l3;

					/* 否则，我们就可以了 */
					fc_require_sleep = false;
				}
			}
			else if (HEAP_XMAX_IS_KEYSHR_LOCKED(fc_infomask))
			{
				LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);

				/* 如果在此期间 xmax 发生了变化，重新开始 */
				if (fc_xmax_infomask_changed(fc_tuple->t_data->t_infomask, fc_infomask) ||
					!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tuple->t_data),
										 fc_xwait))
					goto l3;
				/* 否则，我们就可以了 */
				fc_require_sleep = false;
			}
		}

		/*
		 * 作为与上述检查无关的内容，如果当前事务是元组的唯一锁定者，我们也可以避免休眠。请注意，已持有锁的强度是无关紧要的；这并不是有关在 Xmax 中记录锁（这将无论如何在下面完成）。此外，请注意，我们持有的锁比请求的锁要强的情况已经通过不做任何事情在上面处理。
		 *
		 * 请注意，我们在这里只处理非多事务（non-multixact）情况；MultiXactIdWait 可以独立处理这种情况。
		 */
		if (fc_require_sleep && !(fc_infomask & HEAP_XMAX_IS_MULTI) &&
			TransactionIdIsCurrentTransactionId(fc_xwait))
		{
			/* ... 但是如果在此期间 xmax 发生了变化，重新开始 */
			LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
			if (fc_xmax_infomask_changed(fc_tuple->t_data->t_infomask, fc_infomask) ||
				!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tuple->t_data),
									 fc_xwait))
				goto l3;
			Assert(HEAP_XMAX_IS_LOCKED_ONLY(fc_tuple->t_data->t_infomask));
			fc_require_sleep = false;
		}

		/*
		 * 如果必要，可以在其他事务/多事务上休眠。
		 *
		 * 如果其他事务是已提交的更新/删除，则休眠无济于事：如果我们需要休眠，则应退出以引发错误。
		 *
		 * 到这里为止，我们要么已经获得了缓冲区的独占锁，要么必须等待锁定的事务或多事务；因此在下面，我们确保在休眠之后获取缓冲区锁。
		 */
		if (fc_require_sleep && (fc_result == TM_Updated || fc_result == TM_Deleted))
		{
			LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
			goto failed;
		}
		else if (fc_require_sleep)
		{
			/*
			 * 获取元组锁以建立我们对元组的优先权，否则就死在尝试中。LockTuple 会在我们成为元组的下一个候选者时释放我们。即使我们正在申请共享锁，也必须这样做，但如果我们已经对元组持有较弱的锁则不必。
			 *
			 * 如果我们在下面被迫“重新开始”，我们将保留元组锁；这安排了我们在重新检查元组状态时保持在队列的前面。
			 */
			if (!fc_skip_tuple_lock &&
				!fc_heap_acquire_tuplock(fc_relation, fc_tid, fc_mode, fc_wait_policy,
									  &fc_have_tuple_lock))
			{
				/*
				 * 这只能在 wait_policy 为 Skip 且无法获取锁的情况下发生。
				 */
				fc_result = TM_WouldBlock;
				/* 恢复代码期望保持缓冲区锁 */
				LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
				goto failed;
			}

			if (fc_infomask & HEAP_XMAX_IS_MULTI)
			{
				MultiXactStatus fc_status = fc_get_mxact_status_for_lock(fc_mode, false);

				/* 我们只锁定元组，从不更新它们 */
				if (fc_status >= MultiXactStatusNoKeyUpdate)
					elog(ERROR, "invalid lock mode in heap_lock_tuple");

				/* 等待多事务结束，或死在尝试中 */
				switch (fc_wait_policy)
				{
					case LockWaitBlock:
						fc_MultiXactIdWait((MultiXactId) fc_xwait, fc_status, fc_infomask,
										fc_relation, &fc_tuple->t_self, XLTW_Lock, NULL);
						break;
					case LockWaitSkip:
						if (!fc_ConditionalMultiXactIdWait((MultiXactId) fc_xwait,
														fc_status, fc_infomask, fc_relation,
														NULL))
						{
							fc_result = TM_WouldBlock;
							/* 恢复代码期望保持缓冲区锁 */
							LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
							goto failed;
						}
						break;
					case LockWaitError:
						if (!fc_ConditionalMultiXactIdWait((MultiXactId) fc_xwait,
														fc_status, fc_infomask, fc_relation,
														NULL))
							ereport(ERROR,
									(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
									 errmsg("could not obtain lock on row in relation \"%s\"",
											RelationGetRelationName(fc_relation))));

						break;
				}

				/*
				 * 当然，此时多事务可能尚未完成：如果我们请求的是轻锁模式，其他持有轻锁的事务可能仍然存在，以及我们自身事务或此后端的其他子事务持有的锁。我们需要保留存活的 MultiXact 成员。请注意，在后者情况下这并不是绝对必要的，但这样做更简单。
				 */
			}
			else
			{
				/* 等待常规事务结束，或死在尝试中 */
				switch (fc_wait_policy)
				{
					case LockWaitBlock:
						XactLockTableWait(fc_xwait, fc_relation, &fc_tuple->t_self,
										  XLTW_Lock);
						break;
					case LockWaitSkip:
						if (!ConditionalXactLockTableWait(fc_xwait))
						{
							fc_result = TM_WouldBlock;
							/* 恢复代码期望保持缓冲区锁 */
							LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
							goto failed;
						}
						break;
					case LockWaitError:
						if (!ConditionalXactLockTableWait(fc_xwait))
							ereport(ERROR,
									(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
									 errmsg("could not obtain lock on row in relation \"%s\"",
											RelationGetRelationName(fc_relation))));
						break;
				}
			}

			/* 如果有更新，跟随更新链 */
			if (fc_follow_updates && !HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask))
			{
				TM_Result	fc_res;

				fc_res = fc_heap_lock_updated_tuple(fc_relation, fc_tuple, &fc_t_ctid,
											  GetCurrentTransactionId(),
											  fc_mode);
				if (fc_res != TM_Ok)
				{
					fc_result = fc_res;
					/* 恢复代码期望保持缓冲区锁 */
					LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
					goto failed;
				}
			}

			LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);

			/*
			 * xwait已经完成，但如果xwait刚刚锁定了元组，那么在我们到达这个点之前，其他事务可能会更新这个元组。检查xmax是否改变，如果是这样，重新开始。
			 */
			if (fc_xmax_infomask_changed(fc_tuple->t_data->t_infomask, fc_infomask) ||
				!TransactionIdEquals(HeapTupleHeaderGetRawXmax(fc_tuple->t_data),
									 fc_xwait))
				goto l3;

			if (!(fc_infomask & HEAP_XMAX_IS_MULTI))
			{
				/*
 * 否则，检查它是否已提交或中止。请注意，如果这个元组仅被没有与我们冲突的人锁定，我们就不能在这里；
 * 这将在上面处理。因此，该事务此时必须已经消失。但在多重事务的情况下不要检查这个，
 * 因为某些锁定事务可能仍在运行。
 */
				fc_UpdateXmaxHintBits(fc_tuple->t_data, *fc_buffer, fc_xwait);
			}
		}

		/* 到这里，我们可以确定我们再次持有缓冲区的独占锁 */

		/*
 * 如果先前的 xmax 中止，或者它已提交但只锁定了元组而没有更新；
 * 或者由于某种原因我们完全不需要等待，我们可能会加锁。
 */
		if (!fc_require_sleep ||
			(fc_tuple->t_data->t_infomask & HEAP_XMAX_INVALID) ||
			HEAP_XMAX_IS_LOCKED_ONLY(fc_tuple->t_data->t_infomask) ||
			HeapTupleHeaderIsOnlyLocked(fc_tuple->t_data))
			fc_result = TM_Ok;
		else if (!ItemPointerEquals(&fc_tuple->t_self, &fc_tuple->t_data->t_ctid))
			fc_result = TM_Updated;
		else
			fc_result = TM_Deleted;
	}

failed:
	if (fc_result != TM_Ok)
	{
		Assert(fc_result == TM_SelfModified || fc_result == TM_Updated ||
			   fc_result == TM_Deleted || fc_result == TM_WouldBlock);

		/*
 * 在 LockWaitSkip 语义下锁定元组时，如果我们在上面由于 TM_WouldBlock 而失败，
 * 则可能有并发事务在此期间释放锁并设置 HEAP_XMAX_INVALID。
 * 因此，这个断言与 heap_delete 和 heap_update 中等效的断言稍有不同。
 */
		Assert((fc_result == TM_WouldBlock) ||
			   !(fc_tuple->t_data->t_infomask & HEAP_XMAX_INVALID));
		Assert(fc_result != TM_Updated ||
			   !ItemPointerEquals(&fc_tuple->t_self, &fc_tuple->t_data->t_ctid));
		fc_tmfd->ctid = fc_tuple->t_data->t_ctid;
		fc_tmfd->xmax = HeapTupleHeaderGetUpdateXid(fc_tuple->t_data);
		if (fc_result == TM_SelfModified)
			fc_tmfd->cmax = HeapTupleHeaderGetCmax(fc_tuple->t_data);
		else
			fc_tmfd->cmax = InvalidCommandId;
		goto out_locked;
	}

	/*
 * 如果我们没有固定可见性映射页面，而在我们忙于锁定缓冲区时，该页面已经变得全部可见，
 * 或在我们使其解锁的随后的某个窗口期间，我们将不得不解锁并重新锁定，
 * 以避免在 I/O 期间保持缓冲区锁。这有点不幸，特别是因为我们现在将不得不重新检查元组
 * 是否在我们之下被锁定或更新，但希望这种情况不会发生得很频繁。
 */
	if (fc_vmbuffer == InvalidBuffer && PageIsAllVisible(fc_page))
	{
		LockBuffer(*fc_buffer, BUFFER_LOCK_UNLOCK);
		visibilitymap_pin(fc_relation, fc_block, &fc_vmbuffer);
		LockBuffer(*fc_buffer, BUFFER_LOCK_EXCLUSIVE);
		goto l3;
	}

	fc_xmax = HeapTupleHeaderGetRawXmax(fc_tuple->t_data);
	fc_old_infomask = fc_tuple->t_data->t_infomask;

	/*
	 * 如果这是当前事务中的第一次可能的 multixact 操作，则设置我每个后端的 OldestMemberMXactId 设置。我们可以确定该事务绝不会成为任何旧的 MultiXactIds 的成员。（即使我们最终只是使用下面自己的 TransactionId，我们也必须这样做，因为其他某个后端可能会在之后立即将我们的 XID 纳入 MultiXact。）
	 */
	MultiXactIdSetOldestMember();

	/*
 * 计算要存储到元组中的新 xmax 和 infomask。请注意，我们现在还不修改元组，
 * 因为如果 multixact.c 记录日志，将导致它处于错误状态。
 */
	fc_compute_new_xmax_infomask(fc_xmax, fc_old_infomask, fc_tuple->t_data->t_infomask2,
							  GetCurrentTransactionId(), fc_mode, false,
							  &fc_xid, &fc_new_infomask, &fc_new_infomask2);

	START_CRIT_SECTION();

	/*
 * 存储锁定元组的事务信息。
 *
 * 注意：在这个上下文中，Cmax 是无意义的，所以不要设置它；这样可以避免可能生成无用的组合 CID。
 * 此外，如果我们正在锁定一个之前更新的元组，保留 Cmax 是很重要的。
 *
 * 还要重置 HOT UPDATE 位，但仅在没有更新的情况下；否则我们会破坏 HOT 链。
 */
	fc_tuple->t_data->t_infomask &= ~HEAP_XMAX_BITS;
	fc_tuple->t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED;
	fc_tuple->t_data->t_infomask |= fc_new_infomask;
	fc_tuple->t_data->t_infomask2 |= fc_new_infomask2;
	if (HEAP_XMAX_IS_LOCKED_ONLY(fc_new_infomask))
		HeapTupleHeaderClearHotUpdated(fc_tuple->t_data);
	HeapTupleHeaderSetXmax(fc_tuple->t_data, fc_xid);

	/*
	 * 确保在 t_ctid 中没有向前链链接。请注意，在元组已经被更新的情况下，我们不能覆盖 t_ctid，因为它是由更新者设置的。此外，如果元组已被更新，我们需要跟踪更新链，以锁定元组的新版本。
	 */
	if (HEAP_XMAX_IS_LOCKED_ONLY(fc_new_infomask))
		fc_tuple->t_data->t_ctid = *fc_tid;

	/* 仅在需要时清除可见性图上的全冻结位 */
	if (PageIsAllVisible(fc_page) &&
		visibilitymap_clear(fc_relation, fc_block, fc_vmbuffer,
							VISIBILITYMAP_ALL_FROZEN))
		fc_cleared_all_frozen = true;


	MarkBufferDirty(*fc_buffer);

	/*
	 * XLOG 相关内容。你可能会认为我们不需要一个 XLOG 记录，因为崩溃后没有值得恢复的状态变化。然而你会错的：我们刚刚写入了一个 TransactionId 或 MultiXactId，这可能在磁盘上从未见过，我们需要确保有 XLOG 条目覆盖这些 ID 号码。否则，崩溃后可能会重用相同的 ID，如果这个页面在崩溃之前已经写入磁盘，那将是灾难性的。本质上，即使在这种情况下，我们也必须强制执行 WAL 日志优先于数据的规则。（另外，在 PITR 日志传输或 2PC 环境中，我们无论如何都必须有 XLOG 条目。）
	 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_lock fc_xlrec;
		XLogRecPtr	fc_recptr;

		XLogBeginInsert();
		XLogRegisterBuffer(0, *fc_buffer, REGBUF_STANDARD);

		fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_tuple->t_self);
		fc_xlrec.locking_xid = fc_xid;
		fc_xlrec.infobits_set = fc_compute_infobits(fc_new_infomask,
											  fc_tuple->t_data->t_infomask2);
		fc_xlrec.flags = fc_cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0;
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapLock);

		/* 我们目前不解码行锁，所以没有必要记录来源 */

		fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_LOCK);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	fc_result = TM_Ok;

out_locked:
	LockBuffer(*fc_buffer, BUFFER_LOCK_UNLOCK);

out_unlocked:
	if (BufferIsValid(fc_vmbuffer))
		ReleaseBuffer(fc_vmbuffer);

	/*
	 * 不要在这里更新可见性图。锁定一个元组并不会改变可见性信息。
	 */

	/*
	 * 现在我们已经成功地将元组标记为锁定，我们可以释放 lmgr 元组锁（如果我们有的话）。
	 */
	if (fc_have_tuple_lock)
		UnlockTupleTuplock(fc_relation, fc_tid, fc_mode);

	return fc_result;
}

/*
 * 获取给定元组的重量级锁，以便准备获取其正常的、基于 Xmax 的元组锁。
 *
 * have_tuple_lock 是一个输入和输出参数：输入时，它指示锁是否已被之前获取（在这种情况下，此函数不执行任何操作）。如果此函数返回成功，have_tuple_lock 会被设置为 true。
 *
 * 如果无法获得锁，则返回 false；这只有在 wait_policy 为 Skip 时才会发生。
 */
static bool fc_heap_acquire_tuplock(Relation fc_relation, ItemPointer fc_tid, LockTupleMode fc_mode,
					 LockWaitPolicy fc_wait_policy, bool *fc_have_tuple_lock)
{
	if (*fc_have_tuple_lock)
		return true;

	switch (fc_wait_policy)
	{
		case LockWaitBlock:
			LockTupleTuplock(fc_relation, fc_tid, fc_mode);
			break;

		case LockWaitSkip:
			if (!ConditionalLockTupleTuplock(fc_relation, fc_tid, fc_mode))
				return false;
			break;

		case LockWaitError:
			if (!ConditionalLockTupleTuplock(fc_relation, fc_tid, fc_mode))
				ereport(ERROR,
						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
						 errmsg("could not obtain lock on row in relation \"%s\"",
								RelationGetRelationName(fc_relation))));
			break;
	}
	*fc_have_tuple_lock = true;

	return true;
}

/*
 * 给定一组原始的 Xmax 和 infomask，以及一个以 add_to_xmax 为标识的事务在获取某种模式的新锁，计算在元组上使用的新 Xmax 和相应的 infomasks。
 *
 * 请注意，这可能会产生副作用，例如创建新的 MultiXactId。
 *
 * 大多数调用者在调用此函数之前会调用 HeapTupleSatisfiesUpdate；这将设置 HEAP_XMAX_INVALID 位，如果 xmax 是 MultiXactId 但不再运行。这里存在一个竞争条件，即 MultiXactId 可能从那时起已经完成，但这个不常见的情况要么在这里处理，要么在 MultiXactIdExpand 中处理。
 *
 * 当旧的 xmax 是常规的 TransactionId 时，也可能存在类似的竞争条件。我们再次测试 TransactionIdIsInProgress 只是为了缩小窗口，但仍然可能会产生不必要的 MultiXactId。幸运的是，这没有害处。
 */
static void fc_compute_new_xmax_infomask(TransactionId fc_xmax, uint16 fc_old_infomask,
						  uint16 fc_old_infomask2, TransactionId fc_add_to_xmax,
						  LockTupleMode fc_mode, bool fc_is_update,
						  TransactionId *fc_result_xmax, uint16 *fc_result_infomask,
						  uint16 *fc_result_infomask2)
{
	TransactionId fc_new_xmax;
	uint16		fc_new_infomask,
				fc_new_infomask2;

	Assert(TransactionIdIsCurrentTransactionId(fc_add_to_xmax));

l5:
	fc_new_infomask = 0;
	fc_new_infomask2 = 0;
	if (fc_old_infomask & HEAP_XMAX_INVALID)
	{
		/*
		 * 没有以前的锁；我们只是插入我们自己的 TransactionId。
		 *
		 * 注意，关键是检查这个情况是第一个，因为下面有几个
		 * 块会回到这一块以实现某些优化；在那些情况下，old_infomask 可能包含
		 * 其他脏位，但我们并不关心。
		 */
		if (fc_is_update)
		{
			fc_new_xmax = fc_add_to_xmax;
			if (fc_mode == LockTupleExclusive)
				fc_new_infomask2 |= HEAP_KEYS_UPDATED;
		}
		else
		{
			fc_new_infomask |= HEAP_XMAX_LOCK_ONLY;
			switch (fc_mode)
			{
				case LockTupleKeyShare:
					fc_new_xmax = fc_add_to_xmax;
					fc_new_infomask |= HEAP_XMAX_KEYSHR_LOCK;
					break;
				case LockTupleShare:
					fc_new_xmax = fc_add_to_xmax;
					fc_new_infomask |= HEAP_XMAX_SHR_LOCK;
					break;
				case LockTupleNoKeyExclusive:
					fc_new_xmax = fc_add_to_xmax;
					fc_new_infomask |= HEAP_XMAX_EXCL_LOCK;
					break;
				case LockTupleExclusive:
					fc_new_xmax = fc_add_to_xmax;
					fc_new_infomask |= HEAP_XMAX_EXCL_LOCK;
					fc_new_infomask2 |= HEAP_KEYS_UPDATED;
					break;
				default:
					fc_new_xmax = InvalidTransactionId;	/* 使编译器静默 */
					elog(ERROR, "invalid lock mode");
			}
		}
	}
	else if (fc_old_infomask & HEAP_XMAX_IS_MULTI)
	{
		MultiXactStatus fc_new_status;

		/*
		 * 目前我们不允许为多事务设置 XMAX_COMMITTED，因此
		 * 进行交叉检查。
		 */
		Assert(!(fc_old_infomask & HEAP_XMAX_COMMITTED));

		/*
		 * 一个 multixact 结合 LOCK_ONLY 设置，但没有任何锁位设置
		 * （即一个 pg_upgraded 共享锁定的元组）不可能再运行了。
		 * 这个检查对通过 pg_upgrade 升级的数据库至关重要；
		 * MultiXactIdIsRunning 和 MultiXactIdExpand 假设
		 * 这样的多事务从未被传递。
		 */
		if (HEAP_LOCKED_UPGRADED(fc_old_infomask))
		{
			fc_old_infomask &= ~HEAP_XMAX_IS_MULTI;
			fc_old_infomask |= HEAP_XMAX_INVALID;
			goto l5;
		}

		/*
		 * 如果 XMAX 已经是一个 MultiXactId，那么我们需要扩展它以
		 * 包含 add_to_xmax；但是如果所有成员都是锁定者并且
		 * 全部消失了，我们可以去掉 IS_MULTI 位，只将
		 * add_to_xmax 设置为唯一的锁定者/更新者。如果所有锁定者都消失
		 * 且我们有一个已中止的更新者，我们也可以不需要一个
		 * 多事务。
		 *
		 * 如果我们不这样做，执行 GetMultiXactIdMembers 的成本将由
		 * MultiXactIdExpand 支付，因此这个检查也没有
		 * 额外的工作。
		 */
		if (!MultiXactIdIsRunning(fc_xmax, HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask)))
		{
			if (HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask) ||
				!TransactionIdDidCommit(fc_MultiXactIdGetUpdateXid(fc_xmax,
																fc_old_infomask)))
			{
				/*
				 * 重置这些位并重新启动；否则将继续到
				 * 在下面创建一个新的多事务。
				 */
				fc_old_infomask &= ~HEAP_XMAX_IS_MULTI;
				fc_old_infomask |= HEAP_XMAX_INVALID;
				goto l5;
			}
		}

		fc_new_status = fc_get_mxact_status_for_lock(fc_mode, fc_is_update);

		fc_new_xmax = MultiXactIdExpand((MultiXactId) fc_xmax, fc_add_to_xmax,
									 fc_new_status);
		fc_GetMultiXactIdHintBits(fc_new_xmax, &fc_new_infomask, &fc_new_infomask2);
	}
	else if (fc_old_infomask & HEAP_XMAX_COMMITTED)
	{
		/*
		 * 这是一个已提交的更新，因此我们需要将他保留为
		 * 元组的更新者。
		 */
		MultiXactStatus fc_status;
		MultiXactStatus fc_new_status;

		if (fc_old_infomask2 & HEAP_KEYS_UPDATED)
			fc_status = MultiXactStatusUpdate;
		else
			fc_status = MultiXactStatusNoKeyUpdate;

		fc_new_status = fc_get_mxact_status_for_lock(fc_mode, fc_is_update);

		/*
		 * 由于它没有运行，显然旧的更新者不可能与当前的
		 * 相同，因此我们不需要检查
		 * 上面块中那种情况。
		 */
		fc_new_xmax = MultiXactIdCreate(fc_xmax, fc_status, fc_add_to_xmax, fc_new_status);
		fc_GetMultiXactIdHintBits(fc_new_xmax, &fc_new_infomask, &fc_new_infomask2);
	}
	else if (TransactionIdIsInProgress(fc_xmax))
	{
		/*
		 * 如果 XMAX 是一个有效的、正在进行的 TransactionId，那么我们需要
		 * 创建一个新的 MultiXactId，包含旧的锁定者或
		 * 更新者和我们自己的 TransactionId。
		 */
		MultiXactStatus fc_new_status;
		MultiXactStatus fc_old_status;
		LockTupleMode fc_old_mode;

		if (HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask))
		{
			if (HEAP_XMAX_IS_KEYSHR_LOCKED(fc_old_infomask))
				fc_old_status = MultiXactStatusForKeyShare;
			else if (HEAP_XMAX_IS_SHR_LOCKED(fc_old_infomask))
				fc_old_status = MultiXactStatusForShare;
			else if (HEAP_XMAX_IS_EXCL_LOCKED(fc_old_infomask))
			{
				if (fc_old_infomask2 & HEAP_KEYS_UPDATED)
					fc_old_status = MultiXactStatusForUpdate;
				else
					fc_old_status = MultiXactStatusForNoKeyUpdate;
			}
			else
			{
				/*
				 * LOCK_ONLY 只能单独存在于页面已经被
				 * pg_upgrade 升级的情况下。但是在这种情况下，
				 * TransactionIdIsInProgress() 应该返回 false。
				 * 我们假设在这种情况下它不再被锁定。
				 */
				elog(WARNING, "LOCK_ONLY found for Xid in progress %u", fc_xmax);
				fc_old_infomask |= HEAP_XMAX_INVALID;
				fc_old_infomask &= ~HEAP_XMAX_LOCK_ONLY;
				goto l5;
			}
		}
		else
		{
			/* 这是一个更新，但是哪种更新？ */
			if (fc_old_infomask2 & HEAP_KEYS_UPDATED)
				fc_old_status = MultiXactStatusUpdate;
			else
				fc_old_status = MultiXactStatusNoKeyUpdate;
		}

		fc_old_mode = TUPLOCK_from_mxstatus(fc_old_status);

		/*
		 * 如果要获取的锁与现有锁的 TransactionId 相同，可以进行优化：
		 * 仅考虑这两个锁中最强的作为唯一存在的锁，并重新启动。
		 */
		if (fc_xmax == fc_add_to_xmax)
		{
			/*
			 * 注意，原始元组不可能被更新：我们不会在这里，
			 * 因为元组会是不可见的，我们不会尝试更新它。作为一个细微之处，
			 * 当遍历更新链以锁定元组的未来版本时，这段代码也会运行。
			 * 但我们也不会在这里，因为 add_to_xmax 将与原始
			 * 更新者不同。
			 */
			Assert(HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask));

			/* 获取两者中最强的锁 */
			if (fc_mode < fc_old_mode)
				fc_mode = fc_old_mode;
			/* 不能触碰 is_update */

			fc_old_infomask |= HEAP_XMAX_INVALID;
			goto l5;
		}

		/* 否则，就回退到创建一个新的 multixact */
		fc_new_status = fc_get_mxact_status_for_lock(fc_mode, fc_is_update);
		fc_new_xmax = MultiXactIdCreate(fc_xmax, fc_old_status,
									 fc_add_to_xmax, fc_new_status);
		fc_GetMultiXactIdHintBits(fc_new_xmax, &fc_new_infomask, &fc_new_infomask2);
	}
	else if (!HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask) &&
			 TransactionIdDidCommit(fc_xmax))
	{
		/*
		 * 这是一个已确认的更新，因此我们必须将其保留为元组的更新者。
		 */
		MultiXactStatus fc_status;
		MultiXactStatus fc_new_status;

		if (fc_old_infomask2 & HEAP_KEYS_UPDATED)
			fc_status = MultiXactStatusUpdate;
		else
			fc_status = MultiXactStatusNoKeyUpdate;

		fc_new_status = fc_get_mxact_status_for_lock(fc_mode, fc_is_update);

		/*
		 * 由于它没有运行，显然旧的更新者不可能与当前的
		 * 相同，因此我们不需要检查
		 * 上面块中那种情况。
		 */
		fc_new_xmax = MultiXactIdCreate(fc_xmax, fc_status, fc_add_to_xmax, fc_new_status);
		fc_GetMultiXactIdHintBits(fc_new_xmax, &fc_new_infomask, &fc_new_infomask2);
	}
	else
	{
		/*
		 * 只有当锁定/更新事务在从元组中提取信息掩码时正在运行，
		 * 但在 TransactionIdIsInProgress 运行之前完成时，才能到达这里。
		 * 将其视为根本没有锁定者。
		 */
		fc_old_infomask |= HEAP_XMAX_INVALID;
		goto l5;
	}

	*fc_result_infomask = fc_new_infomask;
	*fc_result_infomask2 = fc_new_infomask2;
	*fc_result_xmax = fc_new_xmax;
}

/*
 * heap_lock_updated_tuple_rec 的子例程。
 *
 * 在给定的 xid 标识的事务持有假设的 multixact 状态的情况下，
 * 当前事务需要等待、失败，还是可以继续，如果它希望获取给定模式的锁？ 
 * "needwait" 被设为 true 如果等待是必要的；如果它可以继续，
 * 则返回 TM_Ok。如果锁已经由当前事务持有，则返回 TM_SelfModified。
 * 如果与另一个事务冲突，则返回不同的 HeapTupleSatisfiesUpdate 返回代码。
 *
 * 之所以说持有状态是假设的，是因为它可能对应于一个单独 Xid 持有的锁，
 * 即不是一个真实的 MultiXactId；我们以这种方式表达是为了简化 API。
 */
static TM_Result fc_test_lockmode_for_conflict(MultiXactStatus fc_status, TransactionId fc_xid,
						   LockTupleMode fc_mode, HeapTuple fc_tup,
						   bool *fc_needwait)
{
	MultiXactStatus fc_wantedstatus;

	*fc_needwait = false;
	fc_wantedstatus = fc_get_mxact_status_for_lock(fc_mode, false);

	/*
	 * 注意：我们 *必须* 在 TransactionIdDidAbort/Commit 之前检查
	 * TransactionIdIsInProgress；有关解释，请参见 heapam_visibility.c 顶部的注释。
	 */
	if (TransactionIdIsCurrentTransactionId(fc_xid))
	{
		/*
		 * 该元组已经被我们自己的事务锁定。这是非常罕见的，
		 * 但如果多个事务试图锁定同一元组的古老版本，则可能发生。
		 */
		return TM_SelfModified;
	}
	else if (TransactionIdIsInProgress(fc_xid))
	{
		/*
		 * 如果锁定事务正在运行，我们的做法取决于锁定模式是否发生冲突：
		 * 如果发生冲突，则我们必须等待它完成；
		 * 否则我们可以继续进行而无需等待以锁定此元组版本。
		 */
		if (DoLockModesConflict(LOCKMODE_from_mxstatus(fc_status),
								LOCKMODE_from_mxstatus(fc_wantedstatus)))
		{
			*fc_needwait = true;
		}

		/*
		 * 如果我们在上面设置了 needwait，则此值无关紧要；
		 * 否则，此值向调用者发出信号，表示可以继续进行。
		 */
		return TM_Ok;
	}
	else if (TransactionIdDidAbort(fc_xid))
		return TM_Ok;
	else if (TransactionIdDidCommit(fc_xid))
	{
		/*
		 * 另一个事务已提交。如果它仅是一个锁定者，那么锁现在完全消失，
		 * 我们可以返回成功；但如果它是一个更新，
		 * 那么我们所做的取决于两个锁定模式是否发生冲突。
		 * 如果发生冲突，我们必须向调用者报告错误。
		 * 但如果没有冲突，我们可以继续进行，允许当前事务锁定元组。
		 *
		 * 注意：我们在这里担心 ISUPDATE 的原因是，
		 * 一旦事务结束，它的所有锁都消失且无意义，因此我们可以忽略它们；
		 * 而它的更新会持续存在。在上述 TransactionIdIsInProgress 的情况下，
		 * 我们不需要检查，因为我们知道锁仍然“活着”，
		 * 因此总是需要检查冲突。
		 */
		if (!ISUPDATE_from_mxstatus(fc_status))
			return TM_Ok;

		if (DoLockModesConflict(LOCKMODE_from_mxstatus(fc_status),
								LOCKMODE_from_mxstatus(fc_wantedstatus)))
		{
			/* 真糟糕 */
			if (!ItemPointerEquals(&fc_tup->t_self, &fc_tup->t_data->t_ctid))
				return TM_Updated;
			else
				return TM_Deleted;
		}

		return TM_Ok;
	}

	/* 未在进行中，未中止，未提交 -- 一定是崩溃了 */
	return TM_Ok;
}


/*
 * heap_lock_updated_tuple 的递归部分
 *
 * 获取 rel 中由 tid 指向的元组，并将其标记为由给定
 * xid 以给定模式锁定；如果该元组被更新，则递归地锁定新
 * 版本。
 */
static TM_Result fc_heap_lock_updated_tuple_rec(Relation fc_rel, ItemPointer fc_tid, TransactionId fc_xid,
							LockTupleMode fc_mode)
{
	TM_Result	fc_result;
	ItemPointerData fc_tupid;
	HeapTupleData fc_mytup;
	Buffer		fc_buf;
	uint16		fc_new_infomask,
				fc_new_infomask2,
				fc_old_infomask,
				fc_old_infomask2;
	TransactionId fc_xmax,
				fc_new_xmax;
	TransactionId fc_priorXmax = InvalidTransactionId;
	bool		fc_cleared_all_frozen = false;
	bool		fc_pinned_desired_page;
	Buffer		fc_vmbuffer = InvalidBuffer;
	BlockNumber fc_block;

	ItemPointerCopy(fc_tid, &fc_tupid);

	for (;;)
	{
		fc_new_infomask = 0;
		fc_new_xmax = InvalidTransactionId;
		fc_block = ItemPointerGetBlockNumber(&fc_tupid);
		ItemPointerCopy(&fc_tupid, &(fc_mytup.t_self));

		if (!heap_fetch(fc_rel, SnapshotAny, &fc_mytup, &fc_buf, false))
		{
			/*
			 * 如果我们未能找到元组的更新版本，那是因为
			 * 它在其创建者事务中止后被清理/修剪掉了。
			 * 所以就像我们到达了链的末端，没有进一步的
			 * 元组可锁定：返回成功给调用者。
			 */
			fc_result = TM_Ok;
			goto out_unlocked;
		}

l4:
		CHECK_FOR_INTERRUPTS();

		/*
		 * 在锁定缓冲区之前，如果有必要，锁定可见性映射页。
		 * 由于我们还没有获得锁，可能还有其他人在更改它，
		 * 所以在我们获得锁之后需要重新检查。
		 */
		if (PageIsAllVisible(BufferGetPage(fc_buf)))
		{
			visibilitymap_pin(fc_rel, fc_block, &fc_vmbuffer);
			fc_pinned_desired_page = true;
		}
		else
			fc_pinned_desired_page = false;

		LockBuffer(fc_buf, BUFFER_LOCK_EXCLUSIVE);

		/*
		 * 如果我们没有锁定可见性映射页，而在我们忙于锁定
		 * 缓冲区时该页变得完全可见，我们必须解锁并重新锁定，
		 * 以避免在 I/O 期间持有缓冲区锁。这有点不幸，但希望
		 * 不会经常发生。
		 *
		 * 注意：在函数的某些路径中，我们可能会在这里持有
		 * 一个 vm 页的锁定，该页可能与此页匹配，也可能不匹配。
		 * 如果此页不是完全可见的，我们将不使用 vm 页，但
		 * 我们会保持这样的锁定直到函数结束。
		 */
		if (!fc_pinned_desired_page && PageIsAllVisible(BufferGetPage(fc_buf)))
		{
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
			visibilitymap_pin(fc_rel, fc_block, &fc_vmbuffer);
			LockBuffer(fc_buf, BUFFER_LOCK_EXCLUSIVE);
		}

		/*
		 * 检查元组的 XMIN 是否与任何先前的 XMAX 相符。
		 * 如果我们到达了链的末端，我们就完成了，所以返回成功。
		 */
		if (TransactionIdIsValid(fc_priorXmax) &&
			!TransactionIdEquals(HeapTupleHeaderGetXmin(fc_mytup.t_data),
								 fc_priorXmax))
		{
			fc_result = TM_Ok;
			goto out_locked;
		}

		/*
		 * 还要检查 Xmin：如果该元组是由一个已中止的
		 * (子)事务创建的，那么我们已经锁定了链中最后
		 * 一个存活的元组，因此完成了，返回成功。
		 */
		if (TransactionIdDidAbort(HeapTupleHeaderGetXmin(fc_mytup.t_data)))
		{
			fc_result = TM_Ok;
			goto out_locked;
		}

		fc_old_infomask = fc_mytup.t_data->t_infomask;
		fc_old_infomask2 = fc_mytup.t_data->t_infomask2;
		fc_xmax = HeapTupleHeaderGetRawXmax(fc_mytup.t_data);

		/*
		 * 如果该元组版本已被某些并发事务更新或锁定，
		 * 我们的处理取决于我们的锁定模式是否与那些事务
		 * 持有的冲突，以及它们的状态。
		 */
		if (!(fc_old_infomask & HEAP_XMAX_INVALID))
		{
			TransactionId fc_rawxmax;
			bool		fc_needwait;

			fc_rawxmax = HeapTupleHeaderGetRawXmax(fc_mytup.t_data);
			if (fc_old_infomask & HEAP_XMAX_IS_MULTI)
			{
				int			fc_nmembers;
				int			fc_i;
				MultiXactMember *fc_members;

				/*
				 * 我们不需要对 pg_upgrade 的元组进行测试：
				 * 这仅适用于更新链中的第一个元组之后的元组。
				 * 说这个链中的第一个元组可能已经在 9.2 中被锁定
				 * 并进行了 pg 升级，但那个已经被我们的调用者
				 * 锁定，而不是我们；而任何后续元组不能被锁定，
				 * 因为我们的调用者必须获得比 pg 升级本身更晚的快照。
				 */
				Assert(!HEAP_LOCKED_UPGRADED(fc_mytup.t_data->t_infomask));

				fc_nmembers = GetMultiXactIdMembers(fc_rawxmax, &fc_members, false,
												 HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask));
				for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
				{
					fc_result = fc_test_lockmode_for_conflict(fc_members[fc_i].status,
														fc_members[fc_i].xid,
														fc_mode,
														&fc_mytup,
														&fc_needwait);

					/*
					 * 如果该元组已经在我们进行此操作的
					 * 先前迭代中被锁定（比如 heap_lock_tuple 
					 * 被迫因 xmax 的变化重新启动锁定循环），
					 * 那么我们已经对该元组版本持有锁定，
					 * 我们不需要做任何事情；这也不是错误条件。
					 * 我们只需跳过这个元组，继续锁定更新链中的
					 * 下一个版本。
					 */
					if (fc_result == TM_SelfModified)
					{
						pfree(fc_members);
						goto next;
					}

					if (fc_needwait)
					{
						LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
						XactLockTableWait(fc_members[fc_i].xid, fc_rel,
										  &fc_mytup.t_self,
										  XLTW_LockUpdated);
						pfree(fc_members);
						goto l4;
					}
					if (fc_result != TM_Ok)
					{
						pfree(fc_members);
						goto out_locked;
					}
				}
				if (fc_members)
					pfree(fc_members);
			}
			else
			{
				MultiXactStatus fc_status;

				/*
				 * 对于非多重 Xmax，我们首先需要通过使用 infomask 位来计算
				 * 相应的 MultiXactStatus。
				 */
				if (HEAP_XMAX_IS_LOCKED_ONLY(fc_old_infomask))
				{
					if (HEAP_XMAX_IS_KEYSHR_LOCKED(fc_old_infomask))
						fc_status = MultiXactStatusForKeyShare;
					else if (HEAP_XMAX_IS_SHR_LOCKED(fc_old_infomask))
						fc_status = MultiXactStatusForShare;
					else if (HEAP_XMAX_IS_EXCL_LOCKED(fc_old_infomask))
					{
						if (fc_old_infomask2 & HEAP_KEYS_UPDATED)
							fc_status = MultiXactStatusForUpdate;
						else
							fc_status = MultiXactStatusForNoKeyUpdate;
					}
					else
					{
						/*
						 * LOCK_ONLY 单独存在（在旧集群中标记为共享锁定的 pg_upgraded 元组）
						 * 不应出现在更新链的中间。
						 */
						elog(ERROR, "invalid lock status in tuple");
					}
				}
				else
				{
					/* 这是一个更新，但是哪种更新？ */
					if (fc_old_infomask2 & HEAP_KEYS_UPDATED)
						fc_status = MultiXactStatusUpdate;
					else
						fc_status = MultiXactStatusNoKeyUpdate;
				}

				fc_result = fc_test_lockmode_for_conflict(fc_status, fc_rawxmax, fc_mode,
													&fc_mytup, &fc_needwait);

				/*
				 * 如果该元组在我们之前的迭代中已经被锁定
				 * （比如 heap_lock_tuple 被迫重新开始锁定循环
				 * 是因为 xmax 的变化），那么我们已经对该元组版本持有锁，
				 * 我们不需要做任何事情；这也不是错误情况。
				 * 我们只需要跳过这个元组并继续锁定更新链中的下一个版本。
				 */
				if (fc_result == TM_SelfModified)
					goto next;

				if (fc_needwait)
				{
					LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
					XactLockTableWait(fc_rawxmax, fc_rel, &fc_mytup.t_self,
									  XLTW_LockUpdated);
					goto l4;
				}
				if (fc_result != TM_Ok)
				{
					goto out_locked;
				}
			}
		}

		/* 计算元组的新 Xmax 和 infomask 值... */
		fc_compute_new_xmax_infomask(fc_xmax, fc_old_infomask, fc_mytup.t_data->t_infomask2,
								  fc_xid, fc_mode, false,
								  &fc_new_xmax, &fc_new_infomask, &fc_new_infomask2);

		if (PageIsAllVisible(BufferGetPage(fc_buf)) &&
			visibilitymap_clear(fc_rel, fc_block, fc_vmbuffer,
								VISIBILITYMAP_ALL_FROZEN))
			fc_cleared_all_frozen = true;

		START_CRIT_SECTION();

		/* ...并设置它们 */
		HeapTupleHeaderSetXmax(fc_mytup.t_data, fc_new_xmax);
		fc_mytup.t_data->t_infomask &= ~HEAP_XMAX_BITS;
		fc_mytup.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		fc_mytup.t_data->t_infomask |= fc_new_infomask;
		fc_mytup.t_data->t_infomask2 |= fc_new_infomask2;

		MarkBufferDirty(fc_buf);

		/* XLOG 相关内容 */
		if (RelationNeedsWAL(fc_rel))
		{
			xl_heap_lock_updated fc_xlrec;
			XLogRecPtr	fc_recptr;
			Page		fc_page = BufferGetPage(fc_buf);

			XLogBeginInsert();
			XLogRegisterBuffer(0, fc_buf, REGBUF_STANDARD);

			fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_mytup.t_self);
			fc_xlrec.xmax = fc_new_xmax;
			fc_xlrec.infobits_set = fc_compute_infobits(fc_new_infomask, fc_new_infomask2);
			fc_xlrec.flags =
				fc_cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0;

			XLogRegisterData((char *) &fc_xlrec, SizeOfHeapLockUpdated);

			fc_recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_LOCK_UPDATED);

			PageSetLSN(fc_page, fc_recptr);
		}

		END_CRIT_SECTION();

next:
		/* 如果我们发现更新链的末尾，我们就完成了。 */
		if (fc_mytup.t_data->t_infomask & HEAP_XMAX_INVALID ||
			HeapTupleHeaderIndicatesMovedPartitions(fc_mytup.t_data) ||
			ItemPointerEquals(&fc_mytup.t_self, &fc_mytup.t_data->t_ctid) ||
			HeapTupleHeaderIsOnlyLocked(fc_mytup.t_data))
		{
			fc_result = TM_Ok;
			goto out_locked;
		}

		/* 尾递归 */
		fc_priorXmax = HeapTupleHeaderGetUpdateXid(fc_mytup.t_data);
		ItemPointerCopy(&(fc_mytup.t_data->t_ctid), &fc_tupid);
		UnlockReleaseBuffer(fc_buf);
	}

	fc_result = TM_Ok;

out_locked:
	UnlockReleaseBuffer(fc_buf);

out_unlocked:
	if (fc_vmbuffer != InvalidBuffer)
		ReleaseBuffer(fc_vmbuffer);

	return fc_result;
}

/*
 * heap_lock_updated_tuple
 *		在锁定更新的元组时跟随更新链，获取
 *		更新版本上的锁（行标记）。
 *
 * 假设初始元组已经被锁定。
 *
 * 此函数不会检查可见性，它只会无条件地将元组标记为锁定。
 * 如果更新链中的任何元组同时被删除（或与正在修改的键一起更新），则睡眠直到
 * 执行该操作的事务完成。
 *
 * 请注意，当我们必须等待其他事务释放元组时，我们不会在遍历的元组上获取沉重的元组锁，
 * 这与 heap_lock_tuple 的做法相反。原因是有多个事务遍历链的情况可能比较少见，
 * 这样饿死的风险不大：在这里的一个前提是正在使用的快照早于创建该元组的更新（因为我们
 * 是从元组的早期版本开始），但同时这样一个事务不能使用可重复读或可串行化隔离级别，
 * 因为这会导致可串行化失败。
 */
static TM_Result fc_heap_lock_updated_tuple(Relation fc_rel, HeapTuple fc_tuple, ItemPointer fc_ctid,
						TransactionId fc_xid, LockTupleMode fc_mode)
{
	/*
	 * 如果元组未被更新，或已移入另一个分区
	 * （实际上是删除），则在此停止。
	 */
	if (!HeapTupleHeaderIndicatesMovedPartitions(fc_tuple->t_data) &&
		!ItemPointerEquals(&fc_tuple->t_self, fc_ctid))
	{
		/*
		 * 如果这是当前事务中第一个可能的多事务操作，则设置我的每个后端的 OldestMemberMXactId
		 * 设置。我们可以确定该事务永远不会成为任何比这更旧的 MultiXactIds 的成员。
		 * （即使我们最终只使用下面自己的 TransactionId，我们也必须这样做，因为
		 * 其他后端可能会在此之后立即将我们的 XID 合并到一个 MultiXact 中。）
		 */
		MultiXactIdSetOldestMember();

		return fc_heap_lock_updated_tuple_rec(fc_rel, fc_ctid, fc_xid, fc_mode);
	}

	/* 没有什么需要锁定 */
	return TM_Ok;
}

/*
 *	heap_finish_speculative - 将投机插入标记为成功
 *
 * 要成功完成投机插入，我们必须从元组中清除投机
 * 令牌。为此，t_ctid 字段的值会被修改为指向
 * 元组本身，这符合新插入普通元组的特征。
 *
 * 注意：在完成或放弃投机插入之前，不应提交。
 * 我们可以隐式地将已提交事务的投机元组视为已完成，
 * 但那样我们必须做好处理已提交元组上的投机令牌的准备。
 * 这并不困难——没有人会查看无效 xmax 的元组的 ctid 字段——
 * 但在完成时清除令牌的成本也不高。
 * 显式确认的 WAL 记录还使逻辑解码更简单。
 */
void heap_finish_speculative(Relation fc_relation, ItemPointer fc_tid)
{
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;

	fc_buffer = ReadBuffer(fc_relation, ItemPointerGetBlockNumber(fc_tid));
	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
	fc_page = (Page) BufferGetPage(fc_buffer);

	fc_offnum = ItemPointerGetOffsetNumber(fc_tid);
	if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
		fc_lp = PageGetItemId(fc_page, fc_offnum);

	if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
		elog(ERROR, "invalid lp");

	fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

	/* SpecTokenOffsetNumber 应当与任何真实偏移区分开 */
	StaticAssertStmt(MaxOffsetNumber < SpecTokenOffsetNumber,
					 "invalid speculative token constant");

	/* 从这里到更改记录时不报告错误 */
	START_CRIT_SECTION();

	Assert(HeapTupleHeaderIsSpeculative(fc_htup));

	MarkBufferDirty(fc_buffer);

	/*
	 * 用指向自身的真实 t_ctid 替换投机插入令牌，
	 * 如常规元组一样。
	 */
	fc_htup->t_ctid = *fc_tid;

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_confirm fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.offnum = ItemPointerGetOffsetNumber(fc_tid);

		XLogBeginInsert();

		/* 我们希望对此进行与普通插入相同的过滤 */
		XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);

		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapConfirm);
		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);

		fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_CONFIRM);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	UnlockReleaseBuffer(fc_buffer);
}

/*
 *	heap_abort_speculative - 杀死一个投机插入的元组
 *
 * 通过将其 xmin 设置为无效，标记在同一命令中
 * 投机插入的元组为死元组。这使其立即在所有事务中
 * 显示为死，包括我们自己的事务。特别是，这使
 * HeapTupleSatisfiesDirty() 将该元组视为死元组，
 * 这样另一后端插入重复键值时就不必不必要地等待
 * 我们的整个事务完成（它只需等待我们的投机插入
 * 完成即可）。
 *
 * 杀死元组可以防止“无原则的死锁”，这种死锁
 * 是由于互相依赖造成的，而用户不可见。根据定义，
 * 无原则的死锁无法通过用户在客户端代码中重新排序
 * 锁获取来防止，因为实现级别的锁获取并不在用户的
 * 直接控制之下。如果投机插入者不采取这种预防措施，
 * 那么在高并发情况下它们可能会彼此死锁，这是不可接受的。
 *
 * 这在某种程度上与 heap_delete 有些冗余，但我们更愿意
 * 有一个要求简单的专用例程。注意，这也用于删除在投机
 * 插入过程中创建的 TOAST 元组。
 *
 * 此例程不会影响逻辑解码，因为它只查看确认记录。
 */
void heap_abort_speculative(Relation fc_relation, ItemPointer fc_tid)
{
	TransactionId fc_xid = GetCurrentTransactionId();
	ItemId		fc_lp;
	HeapTupleData fc_tp;
	Page		fc_page;
	BlockNumber fc_block;
	Buffer		fc_buffer;
	TransactionId fc_prune_xid;

	Assert(ItemPointerIsValid(fc_tid));

	fc_block = ItemPointerGetBlockNumber(fc_tid);
	fc_buffer = ReadBuffer(fc_relation, fc_block);
	fc_page = BufferGetPage(fc_buffer);

	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

	/*
	 * 页面不能全部可见，因为我们刚刚插入到其中，并且
	 * 仍在运行中。
	 */
	Assert(!PageIsAllVisible(fc_page));

	fc_lp = PageGetItemId(fc_page, ItemPointerGetOffsetNumber(fc_tid));
	Assert(ItemIdIsNormal(fc_lp));

	fc_tp.t_tableOid = RelationGetRelid(fc_relation);
	fc_tp.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);
	fc_tp.t_len = ItemIdGetLength(fc_lp);
	fc_tp.t_self = *fc_tid;

	/*
	 * 对元组进行合理性检查，确保它确实是由我们插入的
	 * 投机插入元组。
	 */
	if (fc_tp.t_data->t_choice.t_heap.t_xmin != fc_xid)
		elog(ERROR, "attempted to kill a tuple inserted by another transaction");
	if (!(IsToastRelation(fc_relation) || HeapTupleHeaderIsSpeculative(fc_tp.t_data)))
		elog(ERROR, "attempted to kill a non-speculative tuple");
	Assert(!HeapTupleHeaderIsHeapOnly(fc_tp.t_data));

	/*
	 * 此处无需检查可序列化冲突。组合CID也没有必要。无需提取副本标识，或
	 * 对infomask位进行特殊处理。
	 */

	START_CRIT_SECTION();

	/*
	 * 元组将立即变为死亡状态。通过将xmin设置为TransactionXmin来标记此页为
	 * 修剪候选。在不立即可修剪的情况下，这是我们可以廉价确定的最老的xid，
	 * 并且安全对抗回绕/比表的relfrozenxid更老。为了防御不太可能出现的新关系
	 * 的relfrozenxid比我们的TransactionXmin更新的情况，如果是这样，就使用
	 * relfrozenxid（vacuum无法随后将relfrozenxid移动到TransactionXmin
	 * 之后，因此这里没有竞态条件）。
	 */
	Assert(TransactionIdIsValid(TransactionXmin));
	if (TransactionIdPrecedes(TransactionXmin, fc_relation->rd_rel->relfrozenxid))
		fc_prune_xid = fc_relation->rd_rel->relfrozenxid;
	else
		fc_prune_xid = TransactionXmin;
	PageSetPrunable(fc_page, fc_prune_xid);

	/* 存储删除元组的 xact 的事务信息 */
	fc_tp.t_data->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
	fc_tp.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED;

	/*
	 * 将元组头的xmin设置为InvalidTransactionId。这使得元组立即对所有人不可见。
	 * （特别是，对任何在等待投机性标记的事务，稍后被唤醒。）
	 */
	HeapTupleHeaderSetXmin(fc_tp.t_data, InvalidTransactionId);

	/* 也清除投机性插入标记 */
	fc_tp.t_data->t_ctid = fc_tp.t_self;

	MarkBufferDirty(fc_buffer);

	/*
	 * XLOG相关内容
	 *
	 * 此处生成的WAL记录与heap_delete()匹配。使用相同的恢复
	 * 例程。
	 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_delete fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.flags = XLH_DELETE_IS_SUPER;
		fc_xlrec.infobits_set = fc_compute_infobits(fc_tp.t_data->t_infomask,
											  fc_tp.t_data->t_infomask2);
		fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_tp.t_self);
		fc_xlrec.xmax = fc_xid;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapDelete);
		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);

		/* 没有记录副本标识和复制来源 */

		fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_DELETE);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

	if (HeapTupleHasExternal(&fc_tp))
	{
		Assert(!IsToastRelation(fc_relation));
		heap_toast_delete(fc_relation, &fc_tp, true);
	}

	/*
	 * 从不需要标记元组为失效，因为目录不支持
	 * 投机性插入
	 */

	/* 现在我们可以释放缓冲区 */
	ReleaseBuffer(fc_buffer);

	/* 计数删除，因为我们也计数了插入 */
	pgstat_count_heap_delete(fc_relation);
}

/*
 * heap_inplace_lock - 保护原地更新免受并发 heap_update() 的影响
 *
 * 评估元组的状态是否与无键更新兼容。
 * 当前事务行标记是可以的，任何事务的 KEY SHARE 也是可以的。
 * 如果兼容，则返回 true，并对缓冲区进行独占锁定，
 * 调用者必须通过调用 heap_inplace_update_and_unlock()、调用 heap_inplace_unlock() 或抛出错误来释放。
 * 否则，调用 release_callback(arg)，等待阻塞事务结束，并返回 false。
 *
 * 由于这旨在用于系统目录且 SERIALIZABLE 不涵盖 DDL，因此不保证任何特定的谓词锁定。
 *
 * 可以修改此内容以在进行删除的元组上返回 true，
 * 所有原地更新器都获取与 DROP 冲突的锁。如果显式
 * "DELETE FROM pg_class" 正在进行中，我们会像处理
 * 更新一样等待它。
 *
 * 原地更新字段的读取者期望对这些字段的更改是
 * 持久的。例如，vac_truncate_clog() 通过目录快照从
 * pg_database 元组中读取 datfrozenxid。未来的快照不得
 * 返回相同数据库 OID 的较低 datfrozenxid（在
 * FullTransactionIdPrecedes() 意义上较低）。我们实现这一点是因为在我们对其缓冲区持有锁时，元组的任何更新都不能开始。在像
 * BEGIN;GRANT;CREATE INDEX;COMMIT 这样的情况下，我们正在原地更新仅对
 * 此事务可见的元组。然后 ROLLBACK 是可以丢失
 * 原地更新的一种情况。（在 ROLLBACK 时恢复 relhasindex=false 是可以的，因为
 * 任何并发的 CREATE INDEX 都会被阻塞，然后原地更新已提交的元组。）
 *
 * 原则上，我们可以通过覆盖更新的元组链中的每个元组来避免等待。
 * 读取者的期望仅允许在元组被中止、链的尾部或我们已经更新其 t_ctid 中引用的元组时更新元组。
 * 因此，我们需要从尾到头覆盖元组。这将意味着要么 (a) 在一个临界区内变更所有元组，要么 (b) 接受部分完成的机会。
 * relfrozenxid 更新的部分完成将产生奇怪的后果，即表的下一个 VACUUM 可能会在 vacuum_get_cutoffs() 和完成之间看到表的 relfrozenxid 向前移动。
 */
bool heap_inplace_lock(Relation fc_relation,
				  HeapTuple fc_oldtup_ptr, Buffer fc_buffer,
				  void (*fc_release_callback) (void *), void *fc_arg)
{
	HeapTupleData fc_oldtup = *fc_oldtup_ptr; /* 最小化与 heap_update() 的差异 */
	TM_Result	fc_result;
	bool		fc_ret;

#ifdef USE_ASSERT_CHECKING
	if (RelationGetRelid(fc_relation) == RelationRelationId)
		fc_check_inplace_rel_lock(fc_oldtup_ptr);
#endif

	Assert(BufferIsValid(fc_buffer));

	LockTuple(fc_relation, &fc_oldtup.t_self, InplaceUpdateTupleLock);
	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

	/*----------
	 * 像 heap_update() 一样解释 HeapTupleSatisfiesUpdate()，但有所不同：
	 *
	 * - 无条件等待
	 * - 上面的元组已经被锁定，因为原地更新需要这一点
	 * - 等待后不重新检查头： defer 到下一次迭代更简单
	 * - 即使更新器中止也不再尝试继续：同样
	 * - 不进行交叉检查
	 */
	fc_result = HeapTupleSatisfiesUpdate(&fc_oldtup, GetCurrentCommandId(false),
									  fc_buffer);

	if (fc_result == TM_Invisible)
	{
		/* 没有已知的方法可以发生这种情况 */
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg_internal("attempted to overwrite invisible tuple")));
	}
	else if (fc_result == TM_SelfModified)
	{
		/*
		 * CREATE INDEX 可能在表达式足够愚蠢时达到这里，例如
		 * 调用 e.g. SELECT ... FROM pg_class FOR SHARE。其他 SQL
		 * 语句的 C 代码可能在没有中介 CommandCounterIncrement() 的情况下
		 * 在对同一行进行 heap_update() 后到达这里。
		 */
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("tuple to be updated was already modified by an operation triggered by the current command")));
	}
	else if (fc_result == TM_BeingModified)
	{
		TransactionId fc_xwait;
		uint16		fc_infomask;

		fc_xwait = HeapTupleHeaderGetRawXmax(fc_oldtup.t_data);
		fc_infomask = fc_oldtup.t_data->t_infomask;

		if (fc_infomask & HEAP_XMAX_IS_MULTI)
		{
			LockTupleMode fc_lockmode = LockTupleNoKeyExclusive;
			MultiXactStatus fc_mxact_status = MultiXactStatusNoKeyUpdate;
			int			fc_remain;

			if (fc_DoesMultiXactIdConflict((MultiXactId) fc_xwait, fc_infomask,
										fc_lockmode, NULL))
			{
				LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
				fc_release_callback(fc_arg);
				fc_ret = false;
				fc_MultiXactIdWait((MultiXactId) fc_xwait, fc_mxact_status, fc_infomask,
								fc_relation, &fc_oldtup.t_self, XLTW_Update,
								&fc_remain);
			}
			else
				fc_ret = true;
		}
		else if (TransactionIdIsCurrentTransactionId(fc_xwait))
			fc_ret = true;
		else if (HEAP_XMAX_IS_KEYSHR_LOCKED(fc_infomask))
			fc_ret = true;
		else
		{
			LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			fc_release_callback(fc_arg);
			fc_ret = false;
			XactLockTableWait(fc_xwait, fc_relation, &fc_oldtup.t_self,
							  XLTW_Update);
		}
	}
	else
	{
		fc_ret = (fc_result == TM_Ok);
		if (!fc_ret)
		{
			LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			fc_release_callback(fc_arg);
		}
	}

	/*
	 * GetCatalogSnapshot() 依赖于失效消息来知道何时
	 * 获取新的快照。xwait 的 COMMIT 负责发送该失效消息。
	 * 我们没有获取足够的重量级锁来阻止尚未发送的情况，
	 * 所以我们必须获取一个新的快照，以确保后续尝试有公平的机会。
	 * 虽然如果 xwait 中止我们不需要这个，
	 * 但也不必优化这个。
	 */
	if (!fc_ret)
	{
		UnlockTuple(fc_relation, &fc_oldtup.t_self, InplaceUpdateTupleLock);
		InvalidateCatalogSnapshot();
	}
	return fc_ret;
}

/*
 * heap_inplace_update_and_unlock - systable_inplace_update_finish 的核心
 *
 * 元组大小不能改变，因此其头字段和空位位图（如果有）也不会改变。
 *
 * 由于我们持有 LOCKTAG_TUPLE，更新者没有该元组的本地副本。
 */
void heap_inplace_update_and_unlock(Relation fc_relation,
							   HeapTuple fc_oldtup, HeapTuple fc_tuple,
							   Buffer fc_buffer)
{
	HeapTupleHeader fc_htup = fc_oldtup->t_data;
	uint32		fc_oldlen;
	uint32		fc_newlen;

	Assert(ItemPointerEquals(&fc_oldtup->t_self, &fc_tuple->t_self));
	fc_oldlen = fc_oldtup->t_len - fc_htup->t_hoff;
	fc_newlen = fc_tuple->t_len - fc_tuple->t_data->t_hoff;
	if (fc_oldlen != fc_newlen || fc_htup->t_hoff != fc_tuple->t_data->t_hoff)
		elog(ERROR, "wrong tuple length");

	/* 从这里到更改记录时不报告错误 */
	START_CRIT_SECTION();

	memcpy((char *) fc_htup + fc_htup->t_hoff,
		   (char *) fc_tuple->t_data + fc_tuple->t_data->t_hoff,
		   fc_newlen);

	/*----------
	 * XXX 在这里崩溃可能会导致 datfrozenxid() 超过 relfrozenxid：
	 *
	 * ["D" 是一个 VACUUM (ONLY_DATABASE_STATS)]
	 * ["R" 是一个 VACUUM tbl]
	 * D: vac_update_datfrozenid() -> systable_beginscan(pg_class)
	 * D: systable_getnext() 返回 tbl 的 pg_class 元组
	 * R: memcpy() 到 tbl 的 pg_class 元组中
	 * D: 提升 pg_database.datfrozenxid, XLogInsert(), 完成
	 * [崩溃]
	 * [恢复恢复 datfrozenxid 而不恢复 relfrozenxid]
	 */

	MarkBufferDirty(fc_buffer);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_inplace fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_tuple->t_self);

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapInplace);

		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);
		XLogRegisterBufData(0, (char *) fc_htup + fc_htup->t_hoff, fc_newlen);

		/* 目前未解码原位更新，不记录来源 */

		fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_INPLACE);

		PageSetLSN(BufferGetPage(fc_buffer), fc_recptr);
	}

	END_CRIT_SECTION();

	heap_inplace_unlock(fc_relation, fc_oldtup, fc_buffer);

	/*
	 * 如有必要，发送共享缓存失效。请注意，由于我们仅
	 * 传递元组的新版本，因此这不能用于可能改变 catcache 查找键的
	 * 任何操作。但我们也不打算进行索引更新，因此这更加明显。
	 *
	 * XXX ROLLBACK 将丢弃失效。请参阅测试 inplace-inval.spec。
	 */
	if (!IsBootstrapProcessingMode())
		CacheInvalidateHeapTuple(fc_relation, fc_tuple, NULL);
}

/*
 * heap_inplace_unlock - heap_inplace_lock 的反向操作
 */
void heap_inplace_unlock(Relation fc_relation,
					HeapTuple fc_oldtup, Buffer fc_buffer)
{
	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
	UnlockTuple(fc_relation, &fc_oldtup->t_self, InplaceUpdateTupleLock);
}

/*
 * heap_inplace_update - 不推荐使用
 *
 * 这仅存在于保持模块在旧分支中工作。
 * 受影响的模块应迁移到 systable_inplace_update_begin()。
 */
void heap_inplace_update(Relation fc_relation, HeapTuple fc_tuple)
{
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;
	uint32		fc_oldlen;
	uint32		fc_newlen;

	/*
	 * 目前，我们不允许并行更新。与常规更新不同，
	 * 这不应创建组合 CID，因此可能有可能放松
	 * 这个限制，但没有更多的思考和测试不行。
	 * 无论如何，目前还不清楚这是否有用。
	 */
	if (IsInParallelMode())
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
				 errmsg("cannot update tuples during a parallel operation")));

	fc_buffer = ReadBuffer(fc_relation, ItemPointerGetBlockNumber(&(fc_tuple->t_self)));
	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
	fc_page = (Page) BufferGetPage(fc_buffer);

	fc_offnum = ItemPointerGetOffsetNumber(&(fc_tuple->t_self));
	if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
		fc_lp = PageGetItemId(fc_page, fc_offnum);

	if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
		elog(ERROR, "invalid lp");

	fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

	fc_oldlen = ItemIdGetLength(fc_lp) - fc_htup->t_hoff;
	fc_newlen = fc_tuple->t_len - fc_tuple->t_data->t_hoff;
	if (fc_oldlen != fc_newlen || fc_htup->t_hoff != fc_tuple->t_data->t_hoff)
		elog(ERROR, "wrong tuple length");

	/* 从这里到更改记录时不报告错误 */
	START_CRIT_SECTION();

	memcpy((char *) fc_htup + fc_htup->t_hoff,
		   (char *) fc_tuple->t_data + fc_tuple->t_data->t_hoff,
		   fc_newlen);

	MarkBufferDirty(fc_buffer);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_relation))
	{
		xl_heap_inplace fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.offnum = ItemPointerGetOffsetNumber(&fc_tuple->t_self);

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapInplace);

		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);
		XLogRegisterBufData(0, (char *) fc_htup + fc_htup->t_hoff, fc_newlen);

		/* 目前未解码原位更新，不记录来源 */

		fc_recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_INPLACE);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	UnlockReleaseBuffer(fc_buffer);

	/*
	 * 如有必要，发送共享缓存失效。请注意，由于我们仅
	 * 传递元组的新版本，因此这不能用于可能改变 catcache 查找键的
	 * 任何操作。但我们也不打算进行索引更新，因此这更加明显。
	 */
	if (!IsBootstrapProcessingMode())
		CacheInvalidateHeapTuple(fc_relation, fc_tuple, NULL);
}

#define		FRM_NOOP				0x0001
#define		FRM_INVALIDATE_XMAX		0x0002
#define		FRM_RETURN_IS_XID		0x0004
#define		FRM_RETURN_IS_MULTI		0x0008
#define		FRM_MARK_COMMITTED		0x0010

/*
 * FreezeMultiXactId
 *		确定在一个元组被MultiXactId标记时在冻结过程中该怎么做。
 *
 * "flags"是一个输出值；用于告诉调用者返回时该怎么做。
 *
 * "mxid_oldest_xid_out"是一个输出值；用于跟踪冻结执行后，任何Multixact中仍存在的最旧的Xid。
 *
 * 我们可以在"flags"中设置的可能值：
 * FRM_NOOP
 *		什么都不做 -- 保持现有的Xmax
 * FRM_INVALIDATE_XMAX
 *		将Xmax标记为InvalidTransactionId并设置XMAX_INVALID标志。
 * FRM_RETURN_IS_XID
 *		Xid返回值是一个单个更新Xid，作为xmax设置。
 * FRM_MARK_COMMITTED
 *		Xmax可以标记为HEAP_XMAX_COMMITTED
 * FRM_RETURN_IS_MULTI
 *		返回值是一个新的MultiXactId，作为新的Xmax设置。
 *		(调用者必须使用GetMultiXactIdHintBits获取适当的信息掩码位)
 *
 * "mxid_oldest_xid_out"仅在"flags"包含FRM_NOOP或FRM_RETURN_IS_MULTI时设置，因为我们仅为这些保留一个MultiXactId。
 *
 * 注意：在"flags"中设置FRM_RETURN_IS_MULTI时创建一个_新的_ MultiXactId。
 */
static TransactionId fc_FreezeMultiXactId(MultiXactId fc_multi, uint16 fc_t_infomask,
				  TransactionId fc_relfrozenxid, TransactionId fc_relminmxid,
				  TransactionId fc_cutoff_xid, MultiXactId fc_cutoff_multi,
				  uint16 *fc_flags, TransactionId *fc_mxid_oldest_xid_out)
{
	TransactionId fc_xid = InvalidTransactionId;
	int			fc_i;
	MultiXactMember *fc_members;
	int			fc_nmembers;
	bool		fc_need_replace;
	int			fc_nnewmembers;
	MultiXactMember *fc_newmembers;
	bool		fc_has_lockers;
	TransactionId fc_update_xid;
	bool		fc_update_committed;
	TransactionId fc_temp_xid_out;

	*fc_flags = 0;

	/* 我们应该只在Multis中被调用 */
	Assert(fc_t_infomask & HEAP_XMAX_IS_MULTI);

	if (!MultiXactIdIsValid(fc_multi) ||
		HEAP_LOCKED_UPGRADED(fc_t_infomask))
	{
		/* 确保信息掩码位被适当地设置/重置 */
		*fc_flags |= FRM_INVALIDATE_XMAX;
		return InvalidTransactionId;
	}
	else if (MultiXactIdPrecedes(fc_multi, fc_relminmxid))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg_internal("found multixact %u from before relminmxid %u",
								 fc_multi, fc_relminmxid)));
	else if (MultiXactIdPrecedes(fc_multi, fc_cutoff_multi))
	{
		/*
		 * 这个旧的多项不可能还有成员正在运行，但为了保险起见验证一下。如果它仅仅是一个锁，则可以在没有进一步考虑的情况下将其移除；但如果它包含一个更新，我们可能需要保留它。
		 */
		if (MultiXactIdIsRunning(fc_multi,
								 HEAP_XMAX_IS_LOCKED_ONLY(fc_t_infomask)))
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg_internal("multixact %u from before cutoff %u found to be still running",
									 fc_multi, fc_cutoff_multi)));

		if (HEAP_XMAX_IS_LOCKED_ONLY(fc_t_infomask))
		{
			*fc_flags |= FRM_INVALIDATE_XMAX;
			fc_xid = InvalidTransactionId;
		}
		else
		{
			/* 用更新的xid替换multi */
			fc_xid = fc_MultiXactIdGetUpdateXid(fc_multi, fc_t_infomask);

			/* 不仅仅是一个锁，xid需要有效 */
			Assert(TransactionIdIsValid(fc_xid));

			if (TransactionIdPrecedes(fc_xid, fc_relfrozenxid))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("found update xid %u from before relfrozenxid %u",
										 fc_xid, fc_relfrozenxid)));

			/*
			 * 如果xid比截止日期更旧，它必须已经中止，否则元组会被修剪掉。
			 */
			if (TransactionIdPrecedes(fc_xid, fc_cutoff_xid))
			{
				if (TransactionIdDidCommit(fc_xid))
					ereport(ERROR,
							(errcode(ERRCODE_DATA_CORRUPTED),
							 errmsg_internal("cannot freeze committed update xid %u", fc_xid)));
				*fc_flags |= FRM_INVALIDATE_XMAX;
				fc_xid = InvalidTransactionId;
			}
			else
			{
				*fc_flags |= FRM_RETURN_IS_XID;
			}
		}

		/*
		 * 不要使用FRM_RETURN_IS_XID Xid推回mxid_oldest_xid_out，或者当没有Xids会保留下来时
		 */
		return fc_xid;
	}

	/*
	 * 这个multixact可能有或者可能没有成员仍在运行，但我们知道它是有效的，并且比多项的截止点更新。 
	 * 但是，它的一些成员可能低于Xids的截止点，因此我们需要遍历整个成员数组，以确定是否需要采取任何行动。
	 */

	fc_nmembers =
		GetMultiXactIdMembers(fc_multi, &fc_members, false,
							  HEAP_XMAX_IS_LOCKED_ONLY(fc_t_infomask));
	if (fc_nmembers <= 0)
	{
		/* 没有什么值得保留的 */
		*fc_flags |= FRM_INVALIDATE_XMAX;
		return InvalidTransactionId;
	}

	/* 有没有比截止点更旧的东西？ */
	fc_need_replace = false;
	fc_temp_xid_out = *fc_mxid_oldest_xid_out;	/* FRM_NOOP的初始化 */
	for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
	{
		if (TransactionIdPrecedes(fc_members[fc_i].xid, fc_cutoff_xid))
		{
			fc_need_replace = true;
			break;
		}
		if (TransactionIdPrecedes(fc_members[fc_i].xid, fc_temp_xid_out))
			fc_temp_xid_out = fc_members[fc_i].xid;
	}

	/*
	 * 在最简单的情况下，没有成员比截止点更旧；我们可以保持现有的MultiXactId不变，从而避免对multi进行更昂贵的二次遍历。
	 */
	if (!fc_need_replace)
	{
		/*
		 * 当mxid_oldest_xid_out在这里被推回时，更新Xid很可能是最旧的成员，但我们不依赖于此。
		 */
		*fc_flags |= FRM_NOOP;
		*fc_mxid_oldest_xid_out = fc_temp_xid_out;
		pfree(fc_members);
		return fc_multi;
	}

	/*
	 * 对multi进行更彻底的二次遍历，以确定哪些成员XID实际上需要保留。 
	 * 检查单个成员的精确状态甚至可能表明我们不需要保留任何东西。
	 */
	fc_nnewmembers = 0;
	fc_newmembers = palloc(sizeof(MultiXactMember) * fc_nmembers);
	fc_has_lockers = false;
	fc_update_xid = InvalidTransactionId;
	fc_update_committed = false;
	fc_temp_xid_out = *fc_mxid_oldest_xid_out;	/* FRM_RETURN_IS_MULTI的初始化 */

	for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
	{
		/*
		 * 确定是否保留此成员或忽略它。
		 */
		if (ISUPDATE_from_mxstatus(fc_members[fc_i].status))
		{
			TransactionId fc_xid = fc_members[fc_i].xid;

			Assert(TransactionIdIsValid(fc_xid));
			if (TransactionIdPrecedes(fc_xid, fc_relfrozenxid))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("found update xid %u from before relfrozenxid %u",
										 fc_xid, fc_relfrozenxid)));

			/*
			 * 这是一个更新；我们应该保留它吗？如果事务已知被中止或崩溃，那么忽略它是可以的，否则不可以。请注意，早于 cutoff_xid 的 updater 不能被提交，因为 HeapTupleSatisfiesVacuum 会返回 HEAPTUPLE_DEAD，我们不会尝试冻结这个元组。
			 *
			 * 和所有元组可见性例程一样，在 TransactionIdDidCommit 之前测试 TransactionIdIsInProgress 是至关重要的，因为存在竞态条件，这在 heapam_visibility.c 中有详细解释。
			 */
			if (TransactionIdIsCurrentTransactionId(fc_xid) ||
				TransactionIdIsInProgress(fc_xid))
			{
				Assert(!TransactionIdIsValid(fc_update_xid));
				fc_update_xid = fc_xid;
			}
			else if (TransactionIdDidCommit(fc_xid))
			{
				/*
				 * 事务已提交，因此我们可以告诉调用者设置 HEAP_XMAX_COMMITTED。 （我们之所以能这样做是因为我们知道事务没有在运行。）
				 */
				Assert(!TransactionIdIsValid(fc_update_xid));
				fc_update_committed = true;
				fc_update_xid = fc_xid;
			}
			else
			{
				/*
				 * 没有进行中，也没有提交——必须是被中止或崩溃；我们可以忽略它。
				 */
			}

			/*
			 * 由于元组在 vacuum 修剪时并没有完全移除，更新 Xid 不可能早于 xid cutoff。这样的元组的存在会导致损坏，因此要小心并检查。
			 */
			if (TransactionIdIsValid(fc_update_xid) &&
				TransactionIdPrecedes(fc_update_xid, fc_cutoff_xid))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("found update xid %u from before xid cutoff %u",
										 fc_update_xid, fc_cutoff_xid)));

			/*
			 * 我们确定这是一个对应必须保留的更新的 Xid——将其添加到新成员列表以供日后使用。
			 *
			 * 还可以考虑推迟 temp_xid_out，当我们随后得出需要新的 multi 时（即，当我们设置 FRM_RETURN_IS_MULTI 以便我们的调用者需要保留仍在运行的锁时）。
			 */
			if (TransactionIdIsValid(fc_update_xid))
			{
				fc_newmembers[fc_nnewmembers++] = fc_members[fc_i];
				if (TransactionIdPrecedes(fc_members[fc_i].xid, fc_temp_xid_out))
					fc_temp_xid_out = fc_members[fc_i].xid;
			}
		}
		else
		{
			/* 我们只在锁仍在运行时保留它们 */
			if (TransactionIdIsCurrentTransactionId(fc_members[fc_i].xid) ||
				TransactionIdIsInProgress(fc_members[fc_i].xid))
			{
				/*
				 * 运行中的锁不可能早于 cutoff。
				 *
				 * cutoff 是 <= VACUUM 的 OldestXmin，这也是用于顶层 relfrozenxid_out 跟踪状态的初始值。运行中的锁同样不能早于 VACUUM 的 OldestXmin，因此我们不需要 temp_xid_out 步骤。
				 */
				Assert(TransactionIdIsNormal(fc_members[fc_i].xid));
				Assert(!TransactionIdPrecedes(fc_members[fc_i].xid, fc_cutoff_xid));
				Assert(!TransactionIdPrecedes(fc_members[fc_i].xid,
											  *fc_mxid_oldest_xid_out));
				fc_newmembers[fc_nnewmembers++] = fc_members[fc_i];
				fc_has_lockers = true;
			}
		}
	}

	pfree(fc_members);

	/*
	 * 根据第二次遍历期间收集的信息，确定对调用者的多做什么
	 */
	if (fc_nnewmembers == 0)
	{
		/* 没有什么值得保留的！？告诉调用者移除整个事项 */
		*fc_flags |= FRM_INVALIDATE_XMAX;
		fc_xid = InvalidTransactionId;
		/* 不要推迟 mxid_oldest_xid_out——不会剩下任何 Xids */
	}
	else if (TransactionIdIsValid(fc_update_xid) && !fc_has_lockers)
	{
		/*
		 * 如果只有一个成员并且它是更新，则单独返回它，而不创建新的 Multi。 （XXX 当只有一个剩余锁时我们也可以这样做，但那会使 API 变得复杂；而且，单个更新者的情况更有趣，因为它们的生命周期更长。）
		 */
		Assert(fc_nnewmembers == 1);
		*fc_flags |= FRM_RETURN_IS_XID;
		if (fc_update_committed)
			*fc_flags |= FRM_MARK_COMMITTED;
		fc_xid = fc_update_xid;
		/* 不要使用 FRM_RETURN_IS_XID Xid 推迟 mxid_oldest_xid_out */
	}
	else
	{
		/*
		 * 创建一个新的 multixact，包含之前 multixact 的存活成员，以便设定为元组中的新 Xmax。最古老的存活成员可能会推迟 mxid_oldest_xid_out。
		 */
		fc_xid = MultiXactIdCreateFromMembers(fc_nnewmembers, fc_newmembers);
		*fc_flags |= FRM_RETURN_IS_MULTI;
		*fc_mxid_oldest_xid_out = fc_temp_xid_out;
	}

	pfree(fc_newmembers);

	return fc_xid;
}

/*
 * heap_prepare_freeze_tuple
 *
 * 检查一个元组的任何 XID 字段（xmin, xmax, xvac）是否早于指定的截止 XID 和截止 MultiXactId。如果是，设置足够的状态（在 *frz 输出参数中），以便稍后执行并 WAL 记录我们需要做的事情，并返回 true。如果不需要进行任何更改，则返回 false。此外，如果在执行这些操作后元组将完全冻结，则将 *totally_frozen 设置为 true，如果最终需要更多冻结，则设置为 false。
 *
 * 调用者必须在 heap_execute_freeze_tuple 调用之前自己设置 frz->offset。
 *
 * 假设调用者已使用 HeapTupleSatisfiesVacuum() 检查了元组，并确认它不是 HEAPTUPLE_DEAD（否则我们应该移除元组，而不是冻结它）。
 *
 * *relfrozenxid_out 和 *relminmxid_out 参数是 VACUUM 调用者的堆关系当前目标 relfrozenxid 和 relminmxid。在 VACUUM 完成后，调用者关系中留下的所有未冻结的 XID 或 MXID _必须_ 具有大于或等于 pg_class 中最终 relfrozenxid/relminmxid 值的值。这包括作为任何元组的 xmax 的 MultiXact 成员而保留的 XID。这里的每一次调用在需要时推迟 *relfrozenxid_out 和/或 *relminmxid_out，以避免关系的授权 pg_class 元组中的不安全最终值。
 *
 * 注意：cutoff_xid *必须* <= VACUUM 的 OldestXmin，以确保任何早于它的 XID 不能在任何打开的事务中运行或被视为正在运行。这确保替换不会改变任何人对元组状态的看法。类似地，cutoff_multi 必须 <= VACUUM 的 OldestMxact。
 *
 * 注意：此函数具有副作用：它可能会分配一个新的 MultiXactId。它将在我们稍后在 heap_execute_freeze_tuple 中处理的 *frz 输出内设置为元组的新 xmax。如果元组在共享缓冲区中，则调用者最好已经对其拥有排他锁。
 *
 * 注意：仅设置提示位以指示 XID 已提交/中止是不够的。我们输出的 *frz WAL 记录在 REDO 过程中完全移除了所有旧的 XID。
 */
bool heap_prepare_freeze_tuple(HeapTupleHeader fc_tuple,
						  TransactionId fc_relfrozenxid, TransactionId fc_relminmxid,
						  TransactionId fc_cutoff_xid, TransactionId fc_cutoff_multi,
						  xl_heap_freeze_tuple *fc_frz, bool *fc_totally_frozen,
						  TransactionId *fc_relfrozenxid_out,
						  MultiXactId *fc_relminmxid_out)
{
	bool		fc_changed = false;
	bool		fc_xmax_already_frozen = false;
	bool		fc_xmin_frozen;
	bool		fc_freeze_xmax;
	TransactionId fc_xid;

	fc_frz->frzflags = 0;
	fc_frz->t_infomask2 = fc_tuple->t_infomask2;
	fc_frz->t_infomask = fc_tuple->t_infomask;
	fc_frz->xmax = HeapTupleHeaderGetRawXmax(fc_tuple);

	/*
	 * 处理 xmin。xmin_frozen 有两个稍微不同的含义：在 !XidIsNormal 的情况下，它意味着“xmin 不需要任何冻结”（它已经是一个永久值），而在下面的代码块中，它被设置为 true，表示“xmin 在这里对它所做的事情后不需要冻结”（否则为 false）。就 xmin 而言，我们被允许设置 totally_frozen。在这两种情况下，都不需要处理 relfrozenxid_out，因为无论如何，一旦我们完成，元组的 xmin 将是一个永久值。
	 */
	fc_xid = HeapTupleHeaderGetXmin(fc_tuple);
	if (!TransactionIdIsNormal(fc_xid))
		fc_xmin_frozen = true;
	else
	{
		if (TransactionIdPrecedes(fc_xid, fc_relfrozenxid))
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg_internal("found xmin %u from before relfrozenxid %u",
									 fc_xid, fc_relfrozenxid)));

		fc_xmin_frozen = TransactionIdPrecedes(fc_xid, fc_cutoff_xid);
		if (fc_xmin_frozen)
		{
			if (!TransactionIdDidCommit(fc_xid))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("uncommitted xmin %u from before xid cutoff %u needs to be frozen",
										 fc_xid, fc_cutoff_xid)));

			fc_frz->t_infomask |= HEAP_XMIN_FROZEN;
			fc_changed = true;
		}
		else
		{
			/* xmin 保持未冻结。可能推迟 relfrozenxid_out。 */
			if (TransactionIdPrecedes(fc_xid, *fc_relfrozenxid_out))
				*fc_relfrozenxid_out = fc_xid;
		}
	}

	/*
	 * 处理 xmax。要彻底检查当前的 Xmax 值，我们需要将 MultiXactId 解析为其成员 Xids，以防其中一些低于给定的 Xids 截止值。在这种情况下，那些值也可能需要冻结。此外，如果一个多重事务需要冻结，我们不能简单地将其移除——如果有一个活跃的更新者 Xid，它需要被保留。
	 *
	 * 确保与此保持同步 heap_tuple_would_freeze。
	 */
	fc_xid = HeapTupleHeaderGetRawXmax(fc_tuple);

	if (fc_tuple->t_infomask & HEAP_XMAX_IS_MULTI)
	{
		TransactionId fc_newxmax;
		uint16		fc_flags;
		TransactionId fc_mxid_oldest_xid_out = *fc_relfrozenxid_out;

		fc_newxmax = fc_FreezeMultiXactId(fc_xid, fc_tuple->t_infomask,
									fc_relfrozenxid, fc_relminmxid,
									fc_cutoff_xid, fc_cutoff_multi,
									&fc_flags, &fc_mxid_oldest_xid_out);

		fc_freeze_xmax = (fc_flags & FRM_INVALIDATE_XMAX);

		if (fc_flags & FRM_RETURN_IS_XID)
		{
			/*
			 * xmax 将变成一个更新者 Xid（原始 MultiXact 的更新者成员 Xid 将作为简单 Xid 继续存在于 Xmax 中）。
			 * 这里可能需要回退 relfrozenxid_out，但绝不能回退 relminmxid_out。
			 */
			Assert(!fc_freeze_xmax);
			Assert(TransactionIdIsValid(fc_newxmax));
			if (TransactionIdPrecedes(fc_newxmax, *fc_relfrozenxid_out))
				*fc_relfrozenxid_out = fc_newxmax;

			/*
			 * 注意——其中一些转换仅在我们知道返回的 Xid 是一个元组更新者（即，不仅仅是一个锁）时有效。还要注意，我们之所以不显式关注 HEAP_KEYS_UPDATED，唯一的原因是它存在于 t_infomask2 而不是 t_infomask 中。
			 */
			fc_frz->t_infomask &= ~HEAP_XMAX_BITS;
			fc_frz->xmax = fc_newxmax;
			if (fc_flags & FRM_MARK_COMMITTED)
				fc_frz->t_infomask |= HEAP_XMAX_COMMITTED;
			fc_changed = true;
		}
		else if (fc_flags & FRM_RETURN_IS_MULTI)
		{
			uint16		fc_newbits;
			uint16		fc_newbits2;

			/*
			 * xmax 是一个旧的 MultiXactId，我们必须用一个新的 MultiXactId 替换它，以传递两个或更多原始成员 XIDs。
			 * 这里可能需要回退 relfrozenxid_out，但绝不能回退 relminmxid_out。
			 */
			Assert(!fc_freeze_xmax);
			Assert(MultiXactIdIsValid(fc_newxmax));
			Assert(!MultiXactIdPrecedes(fc_newxmax, *fc_relminmxid_out));
			Assert(TransactionIdPrecedesOrEquals(fc_mxid_oldest_xid_out,
												 *fc_relfrozenxid_out));
			*fc_relfrozenxid_out = fc_mxid_oldest_xid_out;

			/*
			 * 我们无法直接在这里对新的多重事务使用 GetMultiXactIdHintBits；该例程将掩码初始化为全零，这将丢失我们需要的其他位。这样做确保所有无关位保持不变。
			 */
			fc_frz->t_infomask &= ~HEAP_XMAX_BITS;
			fc_frz->t_infomask2 &= ~HEAP_KEYS_UPDATED;
			fc_GetMultiXactIdHintBits(fc_newxmax, &fc_newbits, &fc_newbits2);
			fc_frz->t_infomask |= fc_newbits;
			fc_frz->t_infomask2 |= fc_newbits2;

			fc_frz->xmax = fc_newxmax;

			fc_changed = true;
		}
		else if (fc_flags & FRM_NOOP)
		{
			/*
			 * xmax 是一个 MultiXactId，目前没有任何变化。
			 * 可能需要回退 relminmxid_out、relfrozenxid_out，或者两者一起。
			 */
			Assert(!fc_freeze_xmax);
			Assert(MultiXactIdIsValid(fc_newxmax) && fc_xid == fc_newxmax);
			Assert(TransactionIdPrecedesOrEquals(fc_mxid_oldest_xid_out,
												 *fc_relfrozenxid_out));
			if (MultiXactIdPrecedes(fc_xid, *fc_relminmxid_out))
				*fc_relminmxid_out = fc_xid;
			*fc_relfrozenxid_out = fc_mxid_oldest_xid_out;
		}
		else
		{
			/*
			 * 在 xmax 中不保留任何内容（既没有 Xid 也没有 MultiXactId）。
			 * 不需要回退 relminmxid_out 或 relfrozenxid_out。
			 */
			Assert(fc_freeze_xmax);
			Assert(!TransactionIdIsValid(fc_newxmax));
		}
	}
	else if (TransactionIdIsNormal(fc_xid))
	{
		if (TransactionIdPrecedes(fc_xid, fc_relfrozenxid))
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg_internal("found xmax %u from before relfrozenxid %u",
									 fc_xid, fc_relfrozenxid)));

		if (TransactionIdPrecedes(fc_xid, fc_cutoff_xid))
		{
			/*
			 * 如果我们冻结 xmax，务必确保它不是一个重要的 XID。（注意，一个仅锁定的 xmax 可以独立于已提交状态进行移除，因为一个已提交的锁持有者已经释放了锁）。
			 */
			if (!HEAP_XMAX_IS_LOCKED_ONLY(fc_tuple->t_infomask) &&
				TransactionIdDidCommit(fc_xid))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("cannot freeze committed xmax %u",
										 fc_xid)));
			fc_freeze_xmax = true;
			/* 不需要处理 relfrozenxid_out，因为我们将冻结 xmax */
		}
		else
		{
			fc_freeze_xmax = false;
			if (TransactionIdPrecedes(fc_xid, *fc_relfrozenxid_out))
				*fc_relfrozenxid_out = fc_xid;
		}
	}
	else if ((fc_tuple->t_infomask & HEAP_XMAX_INVALID) ||
			 !TransactionIdIsValid(HeapTupleHeaderGetRawXmax(fc_tuple)))
	{
		fc_freeze_xmax = false;
		fc_xmax_already_frozen = true;
		/* 对于已经冻结的 xmax，不需要处理 relfrozenxid_out */
	}
	else
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg_internal("found xmax %u (infomask 0x%04x) not frozen, not multi, not normal",
								 fc_xid, fc_tuple->t_infomask)));

	if (fc_freeze_xmax)
	{
		Assert(!fc_xmax_already_frozen);

		fc_frz->xmax = InvalidTransactionId;

		/*
		 * 元组可能标记为 XMAX_INVALID 或 XMAX_COMMITTED + LOCKED。标准化为 INVALID，以确保不会让任何人感到困惑。
		 * 同时去掉 HEAP_KEYS_UPDATED 位。
		 */
		fc_frz->t_infomask &= ~HEAP_XMAX_BITS;
		fc_frz->t_infomask |= HEAP_XMAX_INVALID;
		fc_frz->t_infomask2 &= ~HEAP_HOT_UPDATED;
		fc_frz->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		fc_changed = true;
	}

	/*
	 * 旧式的 VACUUM FULL 不再存在，但只要我们支持在数据库中有 MOVED_OFF/MOVED_IN 元组，就必须保留这段代码。
	 */
	if (fc_tuple->t_infomask & HEAP_MOVED)
	{
		fc_xid = HeapTupleHeaderGetXvac(fc_tuple);

		/*
		 * 对于Xvac，我们忽略cutoff_xid并始终执行
		 * 冻结操作。可以实际设置此类值的最旧发布版本是PostgreSQL 8.4，
		 * 因为旧式VACUUM FULL在PostgreSQL 9.0中被移除。请注意，如果我们在这里
		 * 需要尊重cutoff_xid，那么我们需要确保在跳过冻结的情况下清除totally_frozen。
		 *
		 * 不需要处理relfrozenxid_out，因为我们始终冻结xvac。
		 */
		if (TransactionIdIsNormal(fc_xid))
		{
			/*
			 * 如果MOVED_OFF元组不是死的，则xvac事务必须
			 * 失败；而非死的MOVED_IN元组必须意味着
			 * xvac事务成功。
			 */
			if (fc_tuple->t_infomask & HEAP_MOVED_OFF)
				fc_frz->frzflags |= XLH_INVALID_XVAC;
			else
				fc_frz->frzflags |= XLH_FREEZE_XVAC;

			/*
			 * 也可以修复提示位；通常XMIN_COMMITTED
			 * 已经在这里设置，但也有小概率没有。
			 */
			Assert(!(fc_tuple->t_infomask & HEAP_XMIN_INVALID));
			fc_frz->t_infomask |= HEAP_XMIN_COMMITTED;
			fc_changed = true;
		}
	}

	*fc_totally_frozen = (fc_xmin_frozen &&
					   (fc_freeze_xmax || fc_xmax_already_frozen));
	return fc_changed;
}

/*
 * heap_execute_freeze_tuple
 *		执行元组的预备冻结。
 *
 * 调用者负责确保没有其他后端可以访问
 * 支持此元组的存储，要么通过对包含它的
 * 缓存区持有独占锁（这就是懒惰VACUUM所做的），或者将其放在
 * 私有存储中（这就是CLUSTER及其相关操作所做的）。
 *
 * 注意：看起来我们可以在没有独占锁的情况下进行更改，
 * 因为TransactionId的读/写被假定为原子操作。
 * 然而存在一个竞争条件：刚刚提取了一个我们在这里覆盖的旧XID的某个人
 * 可能在我们完成VACUUM之前没有完成对pg_xact的XID检查，并且
 * 可能会截断他所需的pg_xact部分。获取独占锁确保没有其他后端正在检查
 * 元组状态。同时，获取独占锁使调整
 * infomask位变得安全。
 *
 * 注意：此处的所有代码在崩溃恢复期间都必须安全执行！
 */
void heap_execute_freeze_tuple(HeapTupleHeader fc_tuple, xl_heap_freeze_tuple *fc_frz)
{
	HeapTupleHeaderSetXmax(fc_tuple, fc_frz->xmax);

	if (fc_frz->frzflags & XLH_FREEZE_XVAC)
		HeapTupleHeaderSetXvac(fc_tuple, FrozenTransactionId);

	if (fc_frz->frzflags & XLH_INVALID_XVAC)
		HeapTupleHeaderSetXvac(fc_tuple, InvalidTransactionId);

	fc_tuple->t_infomask = fc_frz->t_infomask;
	fc_tuple->t_infomask2 = fc_frz->t_infomask2;
}

/*
 * heap_freeze_tuple
 *		在不进行WAL日志记录的情况下就地冻结元组。
 *
 * 对于像CLUSTER这样的调用者很有用，它们执行自己的WAL日志记录。
 */
bool heap_freeze_tuple(HeapTupleHeader fc_tuple,
				  TransactionId fc_relfrozenxid, TransactionId fc_relminmxid,
				  TransactionId fc_cutoff_xid, TransactionId fc_cutoff_multi)
{
	xl_heap_freeze_tuple fc_frz;
	bool		fc_do_freeze;
	bool		fc_tuple_totally_frozen;
	TransactionId fc_relfrozenxid_out = fc_cutoff_xid;
	MultiXactId fc_relminmxid_out = fc_cutoff_multi;

	fc_do_freeze = heap_prepare_freeze_tuple(fc_tuple,
										  fc_relfrozenxid, fc_relminmxid,
										  fc_cutoff_xid, fc_cutoff_multi,
										  &fc_frz, &fc_tuple_totally_frozen,
										  &fc_relfrozenxid_out, &fc_relminmxid_out);

	/*
	 * 请注意，由于这不是一个进行WAL日志记录的操作，
	 * 因此我们无需填写冻结记录中的偏移量。
	 */

	if (fc_do_freeze)
		heap_execute_freeze_tuple(fc_tuple, &fc_frz);
	return fc_do_freeze;
}

/*
 * 对于给定的MultiXactId，返回应在
 * 元组的infomask中设置的提示位。
 *
 * 通常应在刚刚创建的multixact上调用此函数，
 * 因此在我们的本地缓存中，因此GetMembers调用速度很快。
 */
static void fc_GetMultiXactIdHintBits(MultiXactId fc_multi, uint16 *fc_new_infomask,
					   uint16 *fc_new_infomask2)
{
	int			fc_nmembers;
	MultiXactMember *fc_members;
	int			fc_i;
	uint16		fc_bits = HEAP_XMAX_IS_MULTI;
	uint16		fc_bits2 = 0;
	bool		fc_has_update = false;
	LockTupleMode fc_strongest = LockTupleKeyShare;

	/*
	 * 我们仅在刚创建的multis中使用此函数，因此它们不能是
	 * 升级前的值。
	 */
	fc_nmembers = GetMultiXactIdMembers(fc_multi, &fc_members, false, false);

	for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
	{
		LockTupleMode fc_mode;

		/*
		 * 记住multixact的任何成员持有的最强锁模式。
		 */
		fc_mode = TUPLOCK_from_mxstatus(fc_members[fc_i].status);
		if (fc_mode > fc_strongest)
			fc_strongest = fc_mode;

		/* 查看我们需要哪些其他位 */
		switch (fc_members[fc_i].status)
		{
			case MultiXactStatusForKeyShare:
			case MultiXactStatusForShare:
			case MultiXactStatusForNoKeyUpdate:
				break;

			case MultiXactStatusForUpdate:
				fc_bits2 |= HEAP_KEYS_UPDATED;
				break;

			case MultiXactStatusNoKeyUpdate:
				fc_has_update = true;
				break;

			case MultiXactStatusUpdate:
				fc_bits2 |= HEAP_KEYS_UPDATED;
				fc_has_update = true;
				break;
		}
	}

	if (fc_strongest == LockTupleExclusive ||
		fc_strongest == LockTupleNoKeyExclusive)
		fc_bits |= HEAP_XMAX_EXCL_LOCK;
	else if (fc_strongest == LockTupleShare)
		fc_bits |= HEAP_XMAX_SHR_LOCK;
	else if (fc_strongest == LockTupleKeyShare)
		fc_bits |= HEAP_XMAX_KEYSHR_LOCK;

	if (!fc_has_update)
		fc_bits |= HEAP_XMAX_LOCK_ONLY;

	if (fc_nmembers > 0)
		pfree(fc_members);

	*fc_new_infomask = fc_bits;
	*fc_new_infomask2 = fc_bits2;
}

/*
 * MultiXactIdGetUpdateXid
 *
 * 给定一个multixact Xmax和相应的infomask，且未设置
 * HEAP_XMAX_LOCK_ONLY位，获取并返回更新
 * 事务的Xid。
 *
 * 调用者需要在必要时检查更新事务的状态。
 */
static TransactionId fc_MultiXactIdGetUpdateXid(TransactionId fc_xmax, uint16 fc_t_infomask)
{
	TransactionId fc_update_xact = InvalidTransactionId;
	MultiXactMember *fc_members;
	int			fc_nmembers;

	Assert(!(fc_t_infomask & HEAP_XMAX_LOCK_ONLY));
	Assert(fc_t_infomask & HEAP_XMAX_IS_MULTI);

	/*
	 * 因为我们知道LOCK_ONLY位没有被设置，这不能是来自
	 * pre-pg_upgrade的多重事务。
	 */
	fc_nmembers = GetMultiXactIdMembers(fc_xmax, &fc_members, false, false);

	if (fc_nmembers > 0)
	{
		int			fc_i;

		for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
		{
			/* 忽略锁定者 */
			if (!ISUPDATE_from_mxstatus(fc_members[fc_i].status))
				continue;

			/* 最多只能有一个更新者 */
			Assert(fc_update_xact == InvalidTransactionId);
			fc_update_xact = fc_members[fc_i].xid;
#ifndef USE_ASSERT_CHECKING

			/*
			 * 在启用断言的构建中，遍历整个数组以确保
			 * 没有其他更新者。
			 */
			break;
#endif
		}

		pfree(fc_members);
	}

	return fc_update_xact;
}

/*
 * HeapTupleGetUpdateXid
 *		与上述相同，但使用HeapTupleHeader
 *
 * 另见HeapTupleHeaderGetUpdateXid，可以在未先检查提示位的情况下使用。
 */
TransactionId HeapTupleGetUpdateXid(HeapTupleHeader fc_tuple)
{
	return fc_MultiXactIdGetUpdateXid(HeapTupleHeaderGetRawXmax(fc_tuple),
								   fc_tuple->t_infomask);
}

/*
 * 给定的多重事务是否与当前事务以给定强度获取元组锁发生冲突？
 *
 * 传递的infomask与给定的多重事务在元组头中配对。
 *
 * 如果current_is_member不为NULL，当当前事务是给定多重事务的成员时，它被设置为'true'。
 */
static bool fc_DoesMultiXactIdConflict(MultiXactId fc_multi, uint16 fc_infomask,
						LockTupleMode fc_lockmode, bool *fc_current_is_member)
{
	int			fc_nmembers;
	MultiXactMember *fc_members;
	bool		fc_result = false;
	LOCKMODE	fc_wanted = tupleLockExtraInfo[fc_lockmode].hwlock;

	if (HEAP_LOCKED_UPGRADED(fc_infomask))
		return false;

	fc_nmembers = GetMultiXactIdMembers(fc_multi, &fc_members, false,
									 HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask));
	if (fc_nmembers >= 0)
	{
		int			fc_i;

		for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
		{
			TransactionId fc_memxid;
			LOCKMODE	fc_memlockmode;

			if (fc_result && (fc_current_is_member == NULL || *fc_current_is_member))
				break;

			fc_memlockmode = LOCKMODE_from_mxstatus(fc_members[fc_i].status);

			/* 忽略当前事务的成员（但跟踪它们的存在） */
			fc_memxid = fc_members[fc_i].xid;
			if (TransactionIdIsCurrentTransactionId(fc_memxid))
			{
				if (fc_current_is_member != NULL)
					*fc_current_is_member = true;
				continue;
			}
			else if (fc_result)
				continue;

			/* 忽略与我们想要的锁不冲突的成员 */
			if (!DoLockModesConflict(fc_memlockmode, fc_wanted))
				continue;

			if (ISUPDATE_from_mxstatus(fc_members[fc_i].status))
			{
				/* 忽略已中止的更新者 */
				if (TransactionIdDidAbort(fc_memxid))
					continue;
			}
			else
			{
				/* 忽略不再进行的仅锁定者 */
				if (!TransactionIdIsInProgress(fc_memxid))
					continue;
			}

			/*
			 * 剩下的都是与我们想要的锁冲突的活动锁定者，
			 * 和未中止的更新者。那些与我们想要的相冲突。
			 * 设置为返回true，但继续查找多重事务成员中的当前事务，
			 * 如果需要的话。
			 */
			fc_result = true;
		}
		pfree(fc_members);
	}

	return fc_result;
}

/*
 * Do_MultiXactIdWait
 *		下面两个函数的实际实现。
 *
 * 'multi'、'status'和'infomask'指示等待什么（需要状态以确保
 * 我们只在冲突的成员上休眠，而infomask用于优化多重事务访问，
 * 以防它是仅锁定的多重事务）；'nowait'指示是否使用条件锁获取，
 * 以允许调用者在锁不可用时失败。 'rel'、'ctid'和'oper'用于设置
 * 错误消息的上下文信息。'remaining'如果不为NULL，将接收仍在运行的成员数，
 * 包括我们自己的交易的任何（非中止的）子交易。
 *
 * 我们通过在每个成员上使用XactLockTableWait进行休眠来实现此目的。
 * 然而，当前后端所属的任何成员都*不*被等待；
 * 这不仅是无用的，还会导致XactLockTableWait内部的断言失败。
 * 当返回时，确保所有冲突请求状态的MultiXactId成员的
 * *其他后端的事务*都已结束（并且不能添加新的成员，
 * 因为将成员添加到现有的MultiXactId中并不合法）。
 *
 * 但在我们完成休眠之前，其他人可能已经更改了包含元组的Xmax，
 * 因此调用者需要以某种方式对我们进行迭代。
 *
 * 请注意，在返回false的情况下，剩余成员的数量是
 * 不可信的。
 */
static bool fc_Do_MultiXactIdWait(MultiXactId fc_multi, MultiXactStatus fc_status,
				   uint16 fc_infomask, bool fc_nowait,
				   Relation fc_rel, ItemPointer fc_ctid, XLTW_Oper fc_oper,
				   int *fc_remaining)
{
	bool		fc_result = true;
	MultiXactMember *fc_members;
	int			fc_nmembers;
	int			fc_remain = 0;

	/* 对于pre-pg_upgrade元组，根本不需要休眠 */
	fc_nmembers = HEAP_LOCKED_UPGRADED(fc_infomask) ? -1 :
		GetMultiXactIdMembers(fc_multi, &fc_members, false,
							  HEAP_XMAX_IS_LOCKED_ONLY(fc_infomask));

	if (fc_nmembers >= 0)
	{
		int			fc_i;

		for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
		{
			TransactionId fc_memxid = fc_members[fc_i].xid;
			MultiXactStatus fc_memstatus = fc_members[fc_i].status;

			if (TransactionIdIsCurrentTransactionId(fc_memxid))
			{
				fc_remain++;
				continue;
			}

			if (!DoLockModesConflict(LOCKMODE_from_mxstatus(fc_memstatus),
									 LOCKMODE_from_mxstatus(fc_status)))
			{
				if (fc_remaining && TransactionIdIsInProgress(fc_memxid))
					fc_remain++;
				continue;
			}

			/*
			 * 这个成员与我们的多重事务冲突，所以我们必须休眠（或者
			 * 如果要求避免等待，则返回失败。）
			 *
			 * 请注意，我们自己并没有设置错误上下文回调，
			 * 而是将信息传递给XactLockTableWait。这个
			 * 可能看起来有点浪费，因为上下文是为每个多重事务的成员设置和拆除的，但实际上
			 * 这应该几乎不明显，并且可以避免重复代码。
			 */
			if (fc_nowait)
			{
				fc_result = ConditionalXactLockTableWait(fc_memxid);
				if (!fc_result)
					break;
			}
			else
				XactLockTableWait(fc_memxid, fc_rel, fc_ctid, fc_oper);
		}

		pfree(fc_members);
	}

	if (fc_remaining)
		*fc_remaining = fc_remain;

	return fc_result;
}

/*
 * MultiXactIdWait
 *		在MultiXactId上休眠。
 *
 * 当我们完成睡眠时，其他人可能已经更改了包含元组的Xmax，
 * 所以调用者需要以某种方式对我们进行迭代。
 *
 * 我们返回（在*remaining中，如果不为NULL）仍在运行的成员数量，
 * 包括我们自己的事务的任何（未中止的）子事务。
 */
static void fc_MultiXactIdWait(MultiXactId fc_multi, MultiXactStatus fc_status, uint16 fc_infomask,
				Relation fc_rel, ItemPointer fc_ctid, XLTW_Oper fc_oper,
				int *fc_remaining)
{
	(void) fc_Do_MultiXactIdWait(fc_multi, fc_status, fc_infomask, false,
							  fc_rel, fc_ctid, fc_oper, fc_remaining);
}

/*
 * ConditionalMultiXactIdWait
 *		如上所述，但仅在我们可以在不阻塞的情况下获取锁时才锁定。
 *
 * 当我们完成睡眠时，其他人可能已经更改了包含元组的Xmax，
 * 所以调用者需要以某种方式对我们进行迭代。
 *
 * 如果多重事务现在全部结束，返回true。如果某些
 * 事务可能仍在运行，则返回false。
 *
 * 我们返回（在*remaining中，如果不为NULL）仍在运行的成员数量，
 * 包括我们自己的事务的任何（未中止的）子事务。
 */
static bool fc_ConditionalMultiXactIdWait(MultiXactId fc_multi, MultiXactStatus fc_status,
						   uint16 fc_infomask, Relation fc_rel, int *fc_remaining)
{
	return fc_Do_MultiXactIdWait(fc_multi, fc_status, fc_infomask, true,
							  fc_rel, NULL, XLTW_None, fc_remaining);
}

/*
 * heap_tuple_needs_eventual_freeze
 *
 * 检查元组的任何XID字段（xmin，xmax，xvac）是否最终需要冻结
 * （如果元组未被修剪首先删除）。
 */
bool heap_tuple_needs_eventual_freeze(HeapTupleHeader fc_tuple)
{
	TransactionId fc_xid;

	/*
	 * 如果xmin是正常事务ID，则此元组肯定未被冻结。
	 */
	fc_xid = HeapTupleHeaderGetXmin(fc_tuple);
	if (TransactionIdIsNormal(fc_xid))
		return true;

	/*
	 * 如果xmax是有效的事务或多重事务，则此元组也未被冻结。
	 */
	if (fc_tuple->t_infomask & HEAP_XMAX_IS_MULTI)
	{
		MultiXactId fc_multi;

		fc_multi = HeapTupleHeaderGetRawXmax(fc_tuple);
		if (MultiXactIdIsValid(fc_multi))
			return true;
	}
	else
	{
		fc_xid = HeapTupleHeaderGetRawXmax(fc_tuple);
		if (TransactionIdIsNormal(fc_xid))
			return true;
	}

	if (fc_tuple->t_infomask & HEAP_MOVED)
	{
		fc_xid = HeapTupleHeaderGetXvac(fc_tuple);
		if (TransactionIdIsNormal(fc_xid))
			return true;
	}

	return false;
}

/*
 * heap_tuple_would_freeze
 *
 * 返回值指示heap_prepare_freeze_tuple同级函数在给定相同截止日期的情况下
 * 是否会冻结元组的任何XID/XMID字段。我们还必须处理死元组，因为
 * （xmin，xmax，xvac）字段可以通过修剪整个元组而不是冻结来处理。
 *
 * *relfrozenxid_out和*relminmxid_out输入/输出参数的工作方式与它们
 * 基于的heap_prepare_freeze_tuple参数完全相同。我们
 * 此处从不冻结，这使得跟踪最古老的现存XID/MXID变得简单。
 */
bool heap_tuple_would_freeze(HeapTupleHeader fc_tuple, TransactionId fc_cutoff_xid,
						MultiXactId fc_cutoff_multi,
						TransactionId *fc_relfrozenxid_out,
						MultiXactId *fc_relminmxid_out)
{
	TransactionId fc_xid;
	MultiXactId fc_multi;
	bool		fc_would_freeze = false;

	/* 首先处理xmin */
	fc_xid = HeapTupleHeaderGetXmin(fc_tuple);
	if (TransactionIdIsNormal(fc_xid))
	{
		if (TransactionIdPrecedes(fc_xid, *fc_relfrozenxid_out))
			*fc_relfrozenxid_out = fc_xid;
		if (TransactionIdPrecedes(fc_xid, fc_cutoff_xid))
			fc_would_freeze = true;
	}

	/* 现在处理xmax */
	fc_xid = InvalidTransactionId;
	fc_multi = InvalidMultiXactId;
	if (fc_tuple->t_infomask & HEAP_XMAX_IS_MULTI)
		fc_multi = HeapTupleHeaderGetRawXmax(fc_tuple);
	else
		fc_xid = HeapTupleHeaderGetRawXmax(fc_tuple);

	if (TransactionIdIsNormal(fc_xid))
	{
		/* xmax是非永久性XID */
		if (TransactionIdPrecedes(fc_xid, *fc_relfrozenxid_out))
			*fc_relfrozenxid_out = fc_xid;
		if (TransactionIdPrecedes(fc_xid, fc_cutoff_xid))
			fc_would_freeze = true;
	}
	else if (!MultiXactIdIsValid(fc_multi))
	{
		/* xmax是永久性XID或无效的MultiXactId/XID */
	}
	else if (HEAP_LOCKED_UPGRADED(fc_tuple->t_infomask))
	{
		/* xmax是一个pg_upgrade的MultiXact，不能有更新者XID */
		if (MultiXactIdPrecedes(fc_multi, *fc_relminmxid_out))
			*fc_relminmxid_out = fc_multi;
		/* heap_prepare_freeze_tuple始终冻结pg_upgrade的xmax */
		fc_would_freeze = true;
	}
	else
	{
		/* xmax是一个可能有更新者XID的MultiXactId */
		MultiXactMember *fc_members;
		int			fc_nmembers;

		if (MultiXactIdPrecedes(fc_multi, *fc_relminmxid_out))
			*fc_relminmxid_out = fc_multi;
		if (MultiXactIdPrecedes(fc_multi, fc_cutoff_multi))
			fc_would_freeze = true;

		/* 需要检查mxact的任何成员是否是旧的 */
		fc_nmembers = GetMultiXactIdMembers(fc_multi, &fc_members, false,
										 HEAP_XMAX_IS_LOCKED_ONLY(fc_tuple->t_infomask));

		for (int fc_i = 0; fc_i < fc_nmembers; fc_i++)
		{
			fc_xid = fc_members[fc_i].xid;
			Assert(TransactionIdIsNormal(fc_xid));
			if (TransactionIdPrecedes(fc_xid, *fc_relfrozenxid_out))
				*fc_relfrozenxid_out = fc_xid;
			if (TransactionIdPrecedes(fc_xid, fc_cutoff_xid))
				fc_would_freeze = true;
		}
		if (fc_nmembers > 0)
			pfree(fc_members);
	}

	if (fc_tuple->t_infomask & HEAP_MOVED)
	{
		fc_xid = HeapTupleHeaderGetXvac(fc_tuple);
		if (TransactionIdIsNormal(fc_xid))
		{
			if (TransactionIdPrecedes(fc_xid, *fc_relfrozenxid_out))
				*fc_relfrozenxid_out = fc_xid;
			/* heap_prepare_freeze_tuple始终冻结xvac */
			fc_would_freeze = true;
		}
	}

	return fc_would_freeze;
}

/*
 * 如果'tuple'包含任何可见的XID大于latestRemovedXid，
 * 则将latestRemovedXid向前调整到找到的最大值。
 * 这作为生成热备份冲突的基础，因此
 * 如果一个元组从未可见，则删除它不应与查询冲突。
 */
void HeapTupleHeaderAdvanceLatestRemovedXid(HeapTupleHeader fc_tuple,
									   TransactionId *fc_latestRemovedXid)
{
	TransactionId fc_xmin = HeapTupleHeaderGetXmin(fc_tuple);
	TransactionId fc_xmax = HeapTupleHeaderGetUpdateXid(fc_tuple);
	TransactionId fc_xvac = HeapTupleHeaderGetXvac(fc_tuple);

	if (fc_tuple->t_infomask & HEAP_MOVED)
	{
		if (TransactionIdPrecedes(*fc_latestRemovedXid, fc_xvac))
			*fc_latestRemovedXid = fc_xvac;
	}

	/*
	 * 忽略由中止事务插入的元组，或者如果元组被插入事务
	 * 更新/删除。
	 *
	 * 查找已提交的提示位，或者如果没有设置xmin位，则检查clog。
	 */
	if (HeapTupleHeaderXminCommitted(fc_tuple) ||
		(!HeapTupleHeaderXminInvalid(fc_tuple) && TransactionIdDidCommit(fc_xmin)))
	{
		if (fc_xmax != fc_xmin &&
			TransactionIdFollows(fc_xmax, *fc_latestRemovedXid))
			*fc_latestRemovedXid = fc_xmax;
	}

	/* *latestRemovedXid在结束时可能仍然无效 */
}

#ifdef USE_PREFETCH
/*
 * heap_index_delete_tuples的辅助函数。为
 * prefetch_count缓冲区发出预取请求。prefetch_state跟踪
 * 所有可以预取的缓冲区，以及哪些已经被预取；对该函数的每次调用
 * 将在上一次调用停止的地方继续。
 *
 * 注意：我们期望deltids数组以按堆块分组TID的顺序进行排序，
 * 每个块的所有TID恰好出现在一个组中。
 */
static void fc_index_delete_prefetch_buffer(Relation fc_rel,
							 IndexDeletePrefetchState *fc_prefetch_state,
							 int fc_prefetch_count)
{
	BlockNumber fc_cur_hblkno = fc_prefetch_state->cur_hblkno;
	int			fc_count = 0;
	int			fc_i;
	int			fc_ndeltids = fc_prefetch_state->ndeltids;
	TM_IndexDelete *fc_deltids = fc_prefetch_state->deltids;

	for (fc_i = fc_prefetch_state->next_item;
		 fc_i < fc_ndeltids && fc_count < fc_prefetch_count;
		 fc_i++)
	{
		ItemPointer fc_htid = &fc_deltids[fc_i].tid;

		if (fc_cur_hblkno == InvalidBlockNumber ||
			ItemPointerGetBlockNumber(fc_htid) != fc_cur_hblkno)
		{
			fc_cur_hblkno = ItemPointerGetBlockNumber(fc_htid);
			PrefetchBuffer(fc_rel, MAIN_FORKNUM, fc_cur_hblkno);
			fc_count++;
		}
	}

	/*
	 * 保存预取位置，以便下次可以从该位置继续。
	 */
	fc_prefetch_state->next_item = fc_i;
	fc_prefetch_state->cur_hblkno = fc_cur_hblkno;
}
#endif

/*
 * heap_index_delete_tuples的辅助函数。检查
 * index AM调用者的索引页面中涉及无效TID的索引损坏。
 *
 * 这是进行这些检查的理想场所。索引AM必须在
 * 这里检查的TID所在的索引页面上保持缓冲区锁，因此
 * 我们根本不必担心并发的VACUUM。我们可以确定
 * 当htid直接指向LP_UNUSED项目或
 * 仅堆元组时索引是损坏的，而这在标准的索引扫描中并不是
 * 情况。
 */
static inline void fc_index_delete_check_htid(TM_IndexDeleteOp *fc_delstate,
						Page fc_page, OffsetNumber fc_maxoff,
						ItemPointer fc_htid, TM_IndexStatus *fc_istatus)
{
	OffsetNumber fc_indexpagehoffnum = ItemPointerGetOffsetNumber(fc_htid);
	ItemId		fc_iid;

	Assert(OffsetNumberIsValid(fc_istatus->idxoffnum));

	if (unlikely(fc_indexpagehoffnum > fc_maxoff))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("heap tid from index tuple (%u,%u) points past end of heap page line pointer array at offset %u of block %u in index \"%s\"",
								 ItemPointerGetBlockNumber(fc_htid),
								 fc_indexpagehoffnum,
								 fc_istatus->idxoffnum, fc_delstate->iblknum,
								 RelationGetRelationName(fc_delstate->irel))));

	fc_iid = PageGetItemId(fc_page, fc_indexpagehoffnum);
	if (unlikely(!ItemIdIsUsed(fc_iid)))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("heap tid from index tuple (%u,%u) points to unused heap page item at offset %u of block %u in index \"%s\"",
								 ItemPointerGetBlockNumber(fc_htid),
								 fc_indexpagehoffnum,
								 fc_istatus->idxoffnum, fc_delstate->iblknum,
								 RelationGetRelationName(fc_delstate->irel))));

	if (ItemIdHasStorage(fc_iid))
	{
		HeapTupleHeader fc_htup;

		Assert(ItemIdIsNormal(fc_iid));
		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_iid);

		if (unlikely(HeapTupleHeaderIsHeapOnly(fc_htup)))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("heap tid from index tuple (%u,%u) points to heap-only tuple at offset %u of block %u in index \"%s\"",
									 ItemPointerGetBlockNumber(fc_htid),
									 fc_indexpagehoffnum,
									 fc_istatus->idxoffnum, fc_delstate->iblknum,
									 RelationGetRelationName(fc_delstate->irel))));
	}
}

/*
 * heapam的tableam的index_delete_tuples接口实现。
 *
 * 此辅助函数在索引元组删除期间由索引AM调用。
 * 有关此处实现的接口的解释以及一般操作理论，请参见tableam头部注释。
 * 注意，这里的每个调用都是简单的索引删除调用，或自底向上的索引删除调用。
 *
 * 这可能会生成相当多的I/O，因为我们可能会
 * 从单个索引块删除数百个元组。为了在某种程度上摊销该
 * 成本，这使用预取并结合对
 * 同一堆块的重复访问。
 */
TransactionId heap_index_delete_tuples(Relation fc_rel, TM_IndexDeleteOp *fc_delstate)
{
	/* 初步假设是早期修剪已处理冲突 */
	TransactionId fc_latestRemovedXid = InvalidTransactionId;
	BlockNumber fc_blkno = InvalidBlockNumber;
	Buffer		fc_buf = InvalidBuffer;
	Page		fc_page = NULL;
	OffsetNumber fc_maxoff = InvalidOffsetNumber;
	TransactionId fc_priorXmax;
#ifdef USE_PREFETCH
	IndexDeletePrefetchState fc_prefetch_state;
	int			fc_prefetch_distance;
#endif
	SnapshotData fc_SnapshotNonVacuumable;
	int			fc_finalndeltids = 0,
				fc_nblocksaccessed = 0;

	/* 仅在自底向上的索引删除情况下使用的状态 */
	int			fc_nblocksfavorable = 0;
	int			fc_curtargetfreespace = fc_delstate->bottomupfreespace,
				fc_lastfreespace = 0,
				fc_actualfreespace = 0;
	bool		fc_bottomup_final_block = false;

	InitNonVacuumableSnapshot(fc_SnapshotNonVacuumable, GlobalVisTestFor(fc_rel));

	/* 按TID对调用者的deltids数组进行排序以进行进一步处理 */
	fc_index_delete_sort(fc_delstate);

	/*
	 * 自底向上的案例：按顺序重排deltids数组，以适应
	 * 最大数量的有希望的TID的位置，并确定
	 * 应该考虑存储在排序数组起始位置的
	 * 有利块数。这也将缩小deltids数组，以便
	 * 前期消除完全不利的块。
	 */
	if (fc_delstate->bottomup)
		fc_nblocksfavorable = fc_bottomup_sort_and_shrink(fc_delstate);

#ifdef USE_PREFETCH
	/* 初始化预取状态。 */
	fc_prefetch_state.cur_hblkno = InvalidBlockNumber;
	fc_prefetch_state.next_item = 0;
	fc_prefetch_state.ndeltids = fc_delstate->ndeltids;
	fc_prefetch_state.deltids = fc_delstate->deltids;

	/*
	 * 确定我们将尝试维持的预取距离。
	 *
	 * 由于调用者在 rel 中持有一个缓冲区锁，我们最好确保在调用执行 syscache 查找的代码之前，这不是一个目录关系，以避免死锁的风险。
	 */
	if (IsCatalogRelation(fc_rel))
		fc_prefetch_distance = maintenance_io_concurrency;
	else
		fc_prefetch_distance =
			get_tablespace_maintenance_io_concurrency(fc_rel->rd_rel->reltablespace);

	/* 为自下而上的删除调用者设置初始预取距离上限 */
	if (fc_delstate->bottomup)
	{
		Assert(fc_nblocksfavorable >= 1);
		Assert(fc_nblocksfavorable <= BOTTOMUP_MAX_NBLOCKS);
		fc_prefetch_distance = Min(fc_prefetch_distance, fc_nblocksfavorable);
	}

	/* 开始预取。 */
	fc_index_delete_prefetch_buffer(fc_rel, &fc_prefetch_state, fc_prefetch_distance);
#endif

	/* 遍历 deltids，确定要删除的项，检查它们的范围 */
	Assert(fc_delstate->ndeltids > 0);
	for (int fc_i = 0; fc_i < fc_delstate->ndeltids; fc_i++)
	{
		TM_IndexDelete *fc_ideltid = &fc_delstate->deltids[fc_i];
		TM_IndexStatus *fc_istatus = fc_delstate->status + fc_ideltid->id;
		ItemPointer fc_htid = &fc_ideltid->tid;
		OffsetNumber fc_offnum;

		/*
		 * 读取缓冲区，并在每次遇到新块时执行所需的额外步骤。 如果它和上一个 htid 中的块相同，避免重新获取。
		 */
		if (fc_blkno == InvalidBlockNumber ||
			ItemPointerGetBlockNumber(fc_htid) != fc_blkno)
		{
			/*
			 * 考虑提前放弃自下而上的索引删除调用者，首先。 （仅在之后预取下下一个块，当明确我们至少会访问下一块时。）
			 *
			 * 有时，第一个块为自下而上的调用者释放了如此多的空间，以至于删除过程可以在不再访问更多块的情况下结束。 不过，通常每次自下而上的删除操作需要访问 2 或 3 个块。
			 */
			if (fc_delstate->bottomup)
			{
				/*
				 * 我们经常允许调用者删除一些额外的项，这些项的条目是在调用者的空间目标满足后才到达的。 访问该页面的成本在那时已经支付，因此完成它是有意义的。 当发生这种情况时，我们在这里最终完成一切（通过完成整个自下而上的删除操作，而无需要支付访问更多块的成本）。
				 */
				if (fc_bottomup_final_block)
					break;

				/*
				 * 当我们未能使调用者在处理我们刚刚完成的页面后释放任何额外空间时放弃。 这个规则是我们控制自下而上删除成本的主要方式。
				 */
				if (fc_nblocksaccessed >= 1 && fc_actualfreespace == fc_lastfreespace)
					break;
				fc_lastfreespace = fc_actualfreespace;	/* 下次使用 */

				/*
				 * 删除操作（自下而上）一定会访问下一个块。现在为此做好准备。
				 *
				 * 衰减目标空闲空间，以便我们不会在边际情况下挂得太久。（空间目标只有在允许我们识别出
				 * 由于工作负载特征相符，我们无需访问超过1或2个块来满足调用者时，才真正有用。）
				 *
				 * 然而，当我们遇到连续块时，我们会更有耐心：这些被视为有利块。衰减过程仅在下一个块不是
				 * 有利/连续块时应用。这并不是一般规则的例外；我们仍然坚持在每个访问的块中找到至少一个
				 * 可删除项。有关有利块和堆块局部性的理论的完整细节，请参见 bottomup_nblocksfavorable()。
				 *
				 * 注意：队列中的第一个块始终被视为有利块，因此可以应用衰减的最早时刻就在我们访问
				 * 第二个块之前。Assert() 为我们验证了这一点。
				 */
				Assert(fc_nblocksaccessed > 0 || fc_nblocksfavorable > 0);
				if (fc_nblocksfavorable > 0)
					fc_nblocksfavorable--;
				else
					fc_curtargetfreespace /= 2;
			}

			/* 释放旧缓冲区 */
			if (BufferIsValid(fc_buf))
				UnlockReleaseBuffer(fc_buf);

			fc_blkno = ItemPointerGetBlockNumber(fc_htid);
			fc_buf = ReadBuffer(fc_rel, fc_blkno);
			fc_nblocksaccessed++;
			Assert(!fc_delstate->bottomup ||
				   fc_nblocksaccessed <= BOTTOMUP_MAX_NBLOCKS);

#ifdef USE_PREFETCH

			/*
			 * 为了维持预取距离，每读取一个页面就预取一个页面。
			 */
			fc_index_delete_prefetch_buffer(fc_rel, &fc_prefetch_state, 1);
#endif

			LockBuffer(fc_buf, BUFFER_LOCK_SHARE);

			fc_page = BufferGetPage(fc_buf);
			fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		}

		/*
		 * 在过程中检测涉及具有 TID 的索引页面的索引损坏，该 TID 指向堆中不可能正确的位置。
		 * 我们仅对调用者的索引页面中的实际 TID 执行此操作（而不是通过 HOT 链遍历到的项）。
		 */
		fc_index_delete_check_htid(fc_delstate, fc_page, fc_maxoff, fc_htid, fc_istatus);

		if (fc_istatus->knowndeletable)
			Assert(!fc_delstate->bottomup && !fc_istatus->promising);
		else
		{
			ItemPointerData fc_tmp = *fc_htid;
			HeapTupleData fc_heapTuple;

			/* 这个 HOT 链中的元组是否有不可清理的？ */
			if (heap_hot_search_buffer(&fc_tmp, fc_rel, fc_buf, &fc_SnapshotNonVacuumable,
									   &fc_heapTuple, NULL, true))
				continue;		/* 无法删除条目 */

			/* 调用者将删除，因为整个 HOT 链都可以清理 */
			fc_istatus->knowndeletable = true;

			/* 为自下而上的删除情况维护索引空闲空间信息 */
			if (fc_delstate->bottomup)
			{
				Assert(fc_istatus->freespace > 0);
				fc_actualfreespace += fc_istatus->freespace;
				if (fc_actualfreespace >= fc_curtargetfreespace)
					fc_bottomup_final_block = true;
			}
		}

		/*
		 * 通过使用堆元组头部推进当前值，维护删除操作的 latestRemovedXid 值。
		 * 这在某种程度上是基于修剪 HOT 链的逻辑。
		 */
		fc_offnum = ItemPointerGetOffsetNumber(fc_htid);
		fc_priorXmax = InvalidTransactionId;	/* 无法检查第一个 XMIN */
		for (;;)
		{
			ItemId		fc_lp;
			HeapTupleHeader fc_htup;

			/* 正常性检查（纯粹的偏执） */
			if (fc_offnum < FirstOffsetNumber)
				break;

			/*
			 * 当数组被截断时，可能会出现超出页面行指针数组末尾的偏移量。
			 */
			if (fc_offnum > fc_maxoff)
				break;

			fc_lp = PageGetItemId(fc_page, fc_offnum);
			if (ItemIdIsRedirected(fc_lp))
			{
				fc_offnum = ItemIdGetRedirect(fc_lp);
				continue;
			}

			/*
			 * 我们经常会遇到 LP_DEAD 行指针（特别是带有调用者提前标记为可知可删除的项时）。
			 * 不会检查任何堆元组头部以获取指向 LP_DEAD 项的 htid。这是可以的，因为
			 * 使行指针 LP_DEAD 的早期修剪操作必须已将原始元组头部视为生成
			 * 自己的 latestRemovedXid 值的一部分。
			 *
			 * 依赖于 XLOG_HEAP2_PRUNE 记录的策略与索引清理在所有情况下采用的策略相同。
			 * 索引 VACUUM WAL 记录出于这个原因甚至没有自己的 latestRemovedXid 字段。
			 */
			if (!ItemIdIsNormal(fc_lp))
				break;

			fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

			/*
			 * 检查元组 XMIN 是否与先前的 XMAX 进行比较，如果有的话
			 */
			if (TransactionIdIsValid(fc_priorXmax) &&
				!TransactionIdEquals(HeapTupleHeaderGetXmin(fc_htup), fc_priorXmax))
				break;

			HeapTupleHeaderAdvanceLatestRemovedXid(fc_htup, &fc_latestRemovedXid);

			/*
			 * 如果元组没有进行 HOT 更新，那么我们已经到达这个
			 * HOT 链的末尾。无需访问来自同一更新
			 * 链的后续元组（它们会有自己的索引条目）-- 只需继续
			 * 从索引 AM 调用者移动到下一个 htid。
			 */
			if (!HeapTupleHeaderIsHotUpdated(fc_htup))
				break;

			/* 前进到下一个 HOT 链成员 */
			Assert(ItemPointerGetBlockNumber(&fc_htup->t_ctid) == fc_blkno);
			fc_offnum = ItemPointerGetOffsetNumber(&fc_htup->t_ctid);
			fc_priorXmax = HeapTupleHeaderGetUpdateXid(fc_htup);
		}

		/* 为调用者启用进一步/最终收缩 deltids */
		fc_finalndeltids = fc_i + 1;
	}

	UnlockReleaseBuffer(fc_buf);

	/*
	 * 收缩 deltids 数组，以排除末尾非可删除条目。这
	 * 不仅仅是一个小优化。最终的 deltids 数组大小可能是
	 * 零，适用于自底向上的调用者。索引 AM 明确允许依赖于
	 * ndeltids 在所有情况下都为零，且总可删除条目为零。
	 */
	Assert(fc_finalndeltids > 0 || fc_delstate->bottomup);
	fc_delstate->ndeltids = fc_finalndeltids;

	return fc_latestRemovedXid;
}

/*
 * 针对 index_delete_sort() 的专门内联比较函数
 */
static inline int fc_index_delete_sort_cmp(TM_IndexDelete *fc_deltid1, TM_IndexDelete *fc_deltid2)
{
	ItemPointer fc_tid1 = &fc_deltid1->tid;
	ItemPointer fc_tid2 = &fc_deltid2->tid;

	{
		BlockNumber fc_blk1 = ItemPointerGetBlockNumber(fc_tid1);
		BlockNumber fc_blk2 = ItemPointerGetBlockNumber(fc_tid2);

		if (fc_blk1 != fc_blk2)
			return (fc_blk1 < fc_blk2) ? -1 : 1;
	}
	{
		OffsetNumber fc_pos1 = ItemPointerGetOffsetNumber(fc_tid1);
		OffsetNumber fc_pos2 = ItemPointerGetOffsetNumber(fc_tid2);

		if (fc_pos1 != fc_pos2)
			return (fc_pos1 < fc_pos2) ? -1 : 1;
	}

	Assert(false);

	return 0;
}

/*
 * 根据 TID 对 delstate 中的 deltids 数组进行排序。这为进一步
 * 处理 heap_index_delete_tuples() 做准备。
 *
 * 在某些工作负载下，这一操作会明显消耗 CPU 周期，因此我们
 * 努力进行专门化/微优化。我们使用 shellsort，因为它容易
 * 专门化，编译为相对少量的指令，并且适应于预排序输入/子集
 * （在这里是典型的）。
 */
static void fc_index_delete_sort(TM_IndexDeleteOp *fc_delstate)
{
	TM_IndexDelete *fc_deltids = fc_delstate->deltids;
	int			fc_ndeltids = fc_delstate->ndeltids;
	int			fc_low = 0;

	/*
	 * Shellsort 间隔序列（取自 Sedgewick-Incerpi 论文）。
	 *
	 * 该实现对于数组大小最多可达 ~4500 是快速的。这涵盖了
	 * 所有支持的 BLCKSZ 值。
	 */
	const int	fc_gaps[9] = {1968, 861, 336, 112, 48, 21, 7, 3, 1};

	/* 在这里更改任何内容之前请仔细考虑 -- 保持交换的成本低廉 */
	StaticAssertStmt(sizeof(TM_IndexDelete) <= 8,
					 "element size exceeds 8 bytes");

	for (int fc_g = 0; fc_g < lengthof(fc_gaps); fc_g++)
	{
		for (int fc_hi = fc_gaps[fc_g], fc_i = fc_low + fc_hi; fc_i < fc_ndeltids; fc_i++)
		{
			TM_IndexDelete fc_d = fc_deltids[fc_i];
			int			fc_j = fc_i;

			while (fc_j >= fc_hi && fc_index_delete_sort_cmp(&fc_deltids[fc_j - fc_hi], &fc_d) >= 0)
			{
				fc_deltids[fc_j] = fc_deltids[fc_j - fc_hi];
				fc_j -= fc_hi;
			}
			fc_deltids[fc_j] = fc_d;
		}
	}
}

/*
 * 返回在自下而上的索引删除过程中应被视为有利/连续的块数。
 * 这是从第一块开始并包括第一块的堆块数量。
 *
 * 在自下而上的索引删除过程中，总是至少有一个有利的块。
 * 在最坏的情况下（即完全随机的堆块）线上第一块（唯一的有利块）可以被视为一个
 * 退化的连续块数组，由一个单独的块组成。
 * heap_index_delete_tuples() 将期望这一点。
 *
 * 调用者传递块组，描述最终排序的deltids
 * 将在heap_index_delete_tuples()自下而上的索引删除
 * 处理中进行排序。请注意，deltids不必实际排序（调用者
 * 仅将deltids传递给我们，以便我们可以解释块组）。
 *
 * 你可能会猜测，连续块的存在不会太重要，
 * 因为一般来说，决定我们访问哪些块的主要因素是
 * 有前景的TID的数量，这是来自索引AM的固定提示。
 * 然而，我们并不真正针对一般情况——实际目标是
 * 使我们的行为适应广泛的自然出现条件。
 * 我们应用的大多数启发式方法的效果只能在
 * 聚合中、随着时间的推移和在许多_相关_的自下而上的索引删除
 * 过程中显现出来。
 *
 * 认为某些块是有利的，允许heapam识别和适应
 * 工作负载，其中在自下而上的索引删除过程中访问的堆块
 * 可以连续访问，从本质上讲，每个新访问的块都是自下而上删除刚刚处理完的
 * 块的邻居（或足够接近）。尽早访问更多有利的块
 * 可能会更便宜（例如，在此过程中，而不是跨一系列
 * 相关的自下而上的过程）。无论如何，这可能只是时间问题
 * （或进一步相关的版本波动的问题），在某个自下而上的过程
 * 之前，所有作为一大批有利块一起出现的块都会被访问。
 * 大批的有利块往往要么几乎持续出现，要么甚至一次都不出现（这完全取决于
 * 每个索引的工作负载特征）。
 *
 * 请注意，块组排序顺序应用了一种二的幂次分组
 * 方案，这为持续的块组在一起批处理创造了机会，
 * 至少对于那些自然适合由堆块局部性驱动的工作负载。
 * 这不仅以明显的方式增强了自下而上的堆块处理的空间局部性。
 * 它还实现了访问的时间局部性，因为按堆块编号排序
 * 自然倾向于使自下而上的处理顺序具有确定性。
 *
 * 考虑以下示例，以了解时间局部性
 * 可能的重要性：有一个堆关系，具有几个索引，每个索引的基数低到中等。
 * 它会受到持续非HOT更新的影响。
 * 更新是偏斜的（在主键的某个部分，可能如此）。 
 * 更新语句并未逻辑上修改任何索引（如果有
 * 修改，则根本不会触发自下而上的索引删除）。
 * 自然，对于每个执行heap_update()调用的堆元组，
 * 每个新的索引元组轮次将具有相同的堆TID。
 * 由于这些索引的基数较低且从未被逻辑修改，
 * 在自下而上的删除过程中，heapam处理将按大致顺序访问堆块。
 * 由于自下而上的删除过程在任何给定时刻在每个索引中的行为非常相似，
 * 因此发生了访问的时间局部性。这将需要的缓冲区丢失数量
 * 将访问堆块保持在最低限度。
 */
static int fc_bottomup_nblocksfavorable(IndexDeleteCounts *fc_blockgroups, int fc_nblockgroups,
						  TM_IndexDelete *fc_deltids)
{
	int64		fc_lastblock = -1;
	int			fc_nblocksfavorable = 0;

	Assert(fc_nblockgroups >= 1);
	Assert(fc_nblockgroups <= BOTTOMUP_MAX_NBLOCKS);

	/*
	 * 我们容忍将仅被轻微超出物理顺序访问的堆块。当一对几乎相邻的块恰好落入不同的桶中时，会发生小的波动（可能仅仅是由于npromisingtids的微小差异，桶分类方案没有完全忽略）。我们通过应用一个小的容忍度有效地忽略这些波动。我们使用的具体容忍度有些任意，但在实践中效果很好。
	 */
	for (int fc_b = 0; fc_b < fc_nblockgroups; fc_b++)
	{
		IndexDeleteCounts *fc_group = fc_blockgroups + fc_b;
		TM_IndexDelete *fc_firstdtid = fc_deltids + fc_group->ifirsttid;
		BlockNumber fc_block = ItemPointerGetBlockNumber(&fc_firstdtid->tid);

		if (fc_lastblock != -1 &&
			((int64) fc_block < fc_lastblock - BOTTOMUP_TOLERANCE_NBLOCKS ||
			 (int64) fc_block > fc_lastblock + BOTTOMUP_TOLERANCE_NBLOCKS))
			break;

		fc_nblocksfavorable++;
		fc_lastblock = fc_block;
	}

	/* 始终表示至少有 1 个有利的块 */
	Assert(fc_nblocksfavorable >= 1);

	return fc_nblocksfavorable;
}

/*
 * qsort 比较函数用于 bottomup_sort_and_shrink()
 */
static int fc_bottomup_sort_and_shrink_cmp(const void *fc_arg1, const void *fc_arg2)
{
	const IndexDeleteCounts *fc_group1 = (const IndexDeleteCounts *) fc_arg1;
	const IndexDeleteCounts *fc_group2 = (const IndexDeleteCounts *) fc_arg2;

	/*
	 * 最重要的字段是 npromisingtids（我们反转其顺序以便按降序排序）。
	 *
	 * 调用者应已经将 npromisingtids 字段标准化为 2 的幂值（桶）。
	 */
	if (fc_group1->npromisingtids > fc_group2->npromisingtids)
		return -1;
	if (fc_group1->npromisingtids < fc_group2->npromisingtids)
		return 1;

	/*
	 * 平局打破：按降序 ntids 排序。
	 *
	 * 我们不能期望 ntids 字段是 2 的幂值。我们应该假装它们已经为我们四舍五入了。
	 */
	if (fc_group1->ntids != fc_group2->ntids)
	{
		uint32		fc_ntids1 = pg_nextpower2_32((uint32) fc_group1->ntids);
		uint32		fc_ntids2 = pg_nextpower2_32((uint32) fc_group2->ntids);

		if (fc_ntids1 > fc_ntids2)
			return -1;
		if (fc_ntids1 < fc_ntids2)
			return 1;
	}

	/*
	 * 平局打破：按增序 offset-into-deltids-for-block（块在 deltids 数组中的第一个 TID 的偏移量）排序。
	 *
	 * 这相当于按递增的堆块编号顺序排序（在其他相等的数组子集中）。这种方法使我们避免访问外部 TID。（我们依赖于假设，即在形成每个堆块组的第一个 TID 的偏移量时，deltids 数组是按升序堆 TID 排序的。）
	 */
	if (fc_group1->ifirsttid > fc_group2->ifirsttid)
		return 1;
	if (fc_group1->ifirsttid < fc_group2->ifirsttid)
		return -1;

	pg_unreachable();

	return 0;
}

/*
 * heap_index_delete_tuples() 是用于自下而上删除调用者的辅助函数。
 *
 * 按照自下而上删除所需的顺序对 deltids 数组进行排序。我们被调用时，数组应该已经按 TID 顺序排序。排序过程将来自 deltids 的堆 TID 分组为堆块分组。较早/更有前景的组/块通常是已知拥有最“有前景”TID 的那些。
 *
 * 在状态中设置 deltids 数组的新大小（ndeltids）。当我们返回时，deltids 只会包含来自 BOTTOMUP_MAX_NBLOCKS 最有前景堆块的 TID。这通常意味着 deltids 将缩小到其原始大小的一小部分（我们事先为调用者消除了许多堆块的考虑）。
 *
 * 返回“有利”块的数量。有关定义和完整详细信息，请参见 bottomup_nblocksfavorable()。
 */
static int fc_bottomup_sort_and_shrink(TM_IndexDeleteOp *fc_delstate)
{
	IndexDeleteCounts *fc_blockgroups;
	TM_IndexDelete *fc_reordereddeltids;
	BlockNumber fc_curblock = InvalidBlockNumber;
	int			fc_nblockgroups = 0;
	int			fc_ncopied = 0;
	int			fc_nblocksfavorable = 0;

	Assert(fc_delstate->bottomup);
	Assert(fc_delstate->ndeltids > 0);

	/* 计算每个堆块的 TID 计数 */
	fc_blockgroups = palloc(sizeof(IndexDeleteCounts) * fc_delstate->ndeltids);
	for (int fc_i = 0; fc_i < fc_delstate->ndeltids; fc_i++)
	{
		TM_IndexDelete *fc_ideltid = &fc_delstate->deltids[fc_i];
		TM_IndexStatus *fc_istatus = fc_delstate->status + fc_ideltid->id;
		ItemPointer fc_htid = &fc_ideltid->tid;
		bool		fc_promising = fc_istatus->promising;

		if (fc_curblock != ItemPointerGetBlockNumber(fc_htid))
		{
			/* 新的块组 */
			fc_nblockgroups++;

			Assert(fc_curblock < ItemPointerGetBlockNumber(fc_htid) ||
				   !BlockNumberIsValid(fc_curblock));

			fc_curblock = ItemPointerGetBlockNumber(fc_htid);
			fc_blockgroups[fc_nblockgroups - 1].ifirsttid = fc_i;
			fc_blockgroups[fc_nblockgroups - 1].ntids = 1;
			fc_blockgroups[fc_nblockgroups - 1].npromisingtids = 0;
		}
		else
		{
			fc_blockgroups[fc_nblockgroups - 1].ntids++;
		}

		if (fc_promising)
			fc_blockgroups[fc_nblockgroups - 1].npromisingtids++;
	}

	/*
	 * 我们准备对块组进行排序，以确定访问堆块的最佳顺序。
	 * 但是在此之前，将每个块组中有前景元组的数量四舍五入到下一个二的幂，
	 * 除非它非常低（少于4），在这种情况下我们向上四舍五入到4。
	 * 在选择一对都具有非常低值的块组时，npromisingtids的噪声过大，无法信任。
	 *
	 * 该方案将堆块/块组划分为桶。每个桶包含大致具有相同数量
	 * 前景TID的块。目标是忽略前景条目总数中的相对较小差异，
	 * 这样整个过程就可以稍微考虑堆管理因素（如堆块局部性）。
	 * 这并不真的是一种权衡——我们没有什么可失去的。
	 * 将npromisingtids值的小差异解读为超出噪声的东西是愚蠢的。
	 *
	 * 当对具有相同npromisingtids的块组子集进行排序时，我们对nhtids进行破局，
	 * 但这与npromisingtids存在相同的问题，因此nhtids也受到相同的二的幂桶划分方案的影响。
	 * 我们之所以不以相同的方式修复nhtids，是因为在排序后
	 * 我们需要准确的nhtids值。我们在动态中处理nhtids的桶划分（在排序比较器中）。
	 *
	 * 请参见bottomup_nblocksfavorable()，以获取堆局部性/有利块何时以及如何显著影响
	 * 堆块访问的完整解释。
	 */
	for (int fc_b = 0; fc_b < fc_nblockgroups; fc_b++)
	{
		IndexDeleteCounts *fc_group = fc_blockgroups + fc_b;

		/* 在低npromisingtids的情况下，回退到nhtids会更好 */
		if (fc_group->npromisingtids <= 4)
			fc_group->npromisingtids = 4;
		else
			fc_group->npromisingtids =
				pg_nextpower2_32((uint32) fc_group->npromisingtids);
	}

	/* 排序组并重新排列调用者的deltids数组 */
	qsort(fc_blockgroups, fc_nblockgroups, sizeof(IndexDeleteCounts),
		  fc_bottomup_sort_and_shrink_cmp);
	fc_reordereddeltids = palloc(fc_delstate->ndeltids * sizeof(TM_IndexDelete));

	fc_nblockgroups = Min(BOTTOMUP_MAX_NBLOCKS, fc_nblockgroups);
	/* 确定最终deltids开始时的有利块数量 */
	fc_nblocksfavorable = fc_bottomup_nblocksfavorable(fc_blockgroups, fc_nblockgroups,
												 fc_delstate->deltids);

	for (int fc_b = 0; fc_b < fc_nblockgroups; fc_b++)
	{
		IndexDeleteCounts *fc_group = fc_blockgroups + fc_b;
		TM_IndexDelete *fc_firstdtid = fc_delstate->deltids + fc_group->ifirsttid;

		memcpy(fc_reordereddeltids + fc_ncopied, fc_firstdtid,
			   sizeof(TM_IndexDelete) * fc_group->ntids);
		fc_ncopied += fc_group->ntids;
	}

	/* 将最终分组和排序的TID复制回调用者数组的开头 */
	memcpy(fc_delstate->deltids, fc_reordereddeltids,
		   sizeof(TM_IndexDelete) * fc_ncopied);
	fc_delstate->ndeltids = fc_ncopied;

	pfree(fc_reordereddeltids);
	pfree(fc_blockgroups);

	return fc_nblocksfavorable;
}

/*
 * 执行XLogInsert以进行堆冻结操作。调用者必须已经
 * 修改了缓冲区并标记为脏。
 */
XLogRecPtr log_heap_freeze(Relation fc_reln, Buffer fc_buffer, TransactionId fc_cutoff_xid,
				xl_heap_freeze_tuple *fc_tuples, int fc_ntuples)
{
	xl_heap_freeze_page fc_xlrec;
	XLogRecPtr	fc_recptr;

	/* 调用者不应在非WAL日志记录关系上调用我 */
	Assert(RelationNeedsWAL(fc_reln));
	/* 也不应在没有元组可冻结时调用我 */
	Assert(fc_ntuples > 0);

	fc_xlrec.cutoff_xid = fc_cutoff_xid;
	fc_xlrec.ntuples = fc_ntuples;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, SizeOfHeapFreezePage);

	/*
	 * 冻结计划数组实际上不在缓冲区中，但假装它在。
	 * 当XLogInsert存储整个缓冲区时，冻结计划也不需要存储。
	 */
	XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);
	XLogRegisterBufData(0, (char *) fc_tuples,
						fc_ntuples * sizeof(xl_heap_freeze_tuple));

	fc_recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_FREEZE_PAGE);

	return fc_recptr;
}

/*
 * 执行XLogInsert以进行堆可见操作。'block'是被标记为全可见的块，
 * vm_buffer是包含相应可见性映射块的缓冲区。两者应已
 * 被修改并标记为脏。
 *
 * 如果启用了校验和，我们在必要时还会生成堆缓冲区的全页图像。
 */
XLogRecPtr log_heap_visible(RelFileNode fc_rnode, Buffer fc_heap_buffer, Buffer fc_vm_buffer,
				 TransactionId fc_cutoff_xid, uint8 fc_vmflags)
{
	xl_heap_visible fc_xlrec;
	XLogRecPtr	fc_recptr;
	uint8		fc_flags;

	Assert(BufferIsValid(fc_heap_buffer));
	Assert(BufferIsValid(fc_vm_buffer));

	fc_xlrec.cutoff_xid = fc_cutoff_xid;
	fc_xlrec.flags = fc_vmflags;
	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, SizeOfHeapVisible);

	XLogRegisterBuffer(0, fc_vm_buffer, 0);

	fc_flags = REGBUF_STANDARD;
	if (!XLogHintBitIsNeeded())
		fc_flags |= REGBUF_NO_IMAGE;
	XLogRegisterBuffer(1, fc_heap_buffer, fc_flags);

	fc_recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VISIBLE);

	return fc_recptr;
}

/*
 * 执行XLogInsert以进行堆更新操作。调用者必须已经
 * 修改了缓冲区并将其标记为脏。
 */
static XLogRecPtr fc_log_heap_update(Relation fc_reln, Buffer fc_oldbuf,
				Buffer fc_newbuf, HeapTuple fc_oldtup, HeapTuple fc_newtup,
				HeapTuple fc_old_key_tuple,
				bool fc_all_visible_cleared, bool fc_new_all_visible_cleared)
{
	xl_heap_update fc_xlrec;
	xl_heap_header fc_xlhdr;
	xl_heap_header fc_xlhdr_idx;
	uint8		fc_info;
	uint16		fc_prefix_suffix[2];
	uint16		fc_prefixlen = 0,
				fc_suffixlen = 0;
	XLogRecPtr	fc_recptr;
	Page		fc_page = BufferGetPage(fc_newbuf);
	bool		fc_need_tuple_data = RelationIsLogicallyLogged(fc_reln);
	bool		fc_init;
	int			fc_bufflags;

	/* 调用者不应在非WAL日志记录关系上调用我 */
	Assert(RelationNeedsWAL(fc_reln));

	XLogBeginInsert();

	if (HeapTupleIsHeapOnly(fc_newtup))
		fc_info = XLOG_HEAP_HOT_UPDATE;
	else
		fc_info = XLOG_HEAP_UPDATE;

	/*
	 * 如果旧元组和新元组在同一页上，我们只需记录
	 * 新元组中被更改的部分。这可以节省我们需要写入的
	 * WAL 量。目前，我们仅统计元组开头和结尾的
	 * 未更改字节。这检查起来很快，并且
	 * 完美覆盖了仅更新一个字段的常见情况。
	 *
	 * 即使旧元组和新元组在不同页面上，我们也可以这样做，
	 * 但前提是我们不需要对旧页面进行全页图像，这
	 * 很难提前知道。此外，如果旧元组由于某种原因
	 * 损坏，它将允许损坏传播到新页面，
	 * 所以最好避免。根据一般假设，大多数
	 * 更新倾向于在同一页面上创建新元组版本，
	 * 在不同页面上进行此操作并没有太大好处。
	 *
	 * 如果我们对新页面进行全页图像，则跳过此步骤，因为在这种情况下
	 * 不会将新元组包含在WAL记录中。此外，
	 * 如果 wal_level='logical'，也禁用此操作，因为逻辑解码需要能够
	 * 单独从 WAL 记录中读取新元组。
	 */
	if (fc_oldbuf == fc_newbuf && !fc_need_tuple_data &&
		!XLogCheckBufferNeedsBackup(fc_newbuf))
	{
		char	   *fc_oldp = (char *) fc_oldtup->t_data + fc_oldtup->t_data->t_hoff;
		char	   *fc_newp = (char *) fc_newtup->t_data + fc_newtup->t_data->t_hoff;
		int			fc_oldlen = fc_oldtup->t_len - fc_oldtup->t_data->t_hoff;
		int			fc_newlen = fc_newtup->t_len - fc_newtup->t_data->t_hoff;

		/* 检查旧元组和新元组之间的公共前缀 */
		for (fc_prefixlen = 0; fc_prefixlen < Min(fc_oldlen, fc_newlen); fc_prefixlen++)
		{
			if (fc_newp[fc_prefixlen] != fc_oldp[fc_prefixlen])
				break;
		}

		/*
		 * 存储前缀的长度需要 2 字节，因此我们需要保存
		 * 至少 3 字节，否则没有意义。
		 */
		if (fc_prefixlen < 3)
			fc_prefixlen = 0;

		/* 尾部同理 */
		for (fc_suffixlen = 0; fc_suffixlen < Min(fc_oldlen, fc_newlen) - fc_prefixlen; fc_suffixlen++)
		{
			if (fc_newp[fc_newlen - fc_suffixlen - 1] != fc_oldp[fc_oldlen - fc_suffixlen - 1])
				break;
		}
		if (fc_suffixlen < 3)
			fc_suffixlen = 0;
	}

	/* 准备主 WAL 数据链 */
	fc_xlrec.flags = 0;
	if (fc_all_visible_cleared)
		fc_xlrec.flags |= XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED;
	if (fc_new_all_visible_cleared)
		fc_xlrec.flags |= XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED;
	if (fc_prefixlen > 0)
		fc_xlrec.flags |= XLH_UPDATE_PREFIX_FROM_OLD;
	if (fc_suffixlen > 0)
		fc_xlrec.flags |= XLH_UPDATE_SUFFIX_FROM_OLD;
	if (fc_need_tuple_data)
	{
		fc_xlrec.flags |= XLH_UPDATE_CONTAINS_NEW_TUPLE;
		if (fc_old_key_tuple)
		{
			if (fc_reln->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
				fc_xlrec.flags |= XLH_UPDATE_CONTAINS_OLD_TUPLE;
			else
				fc_xlrec.flags |= XLH_UPDATE_CONTAINS_OLD_KEY;
		}
	}

	/* 如果新元组是页面上的唯一且第一个元组... */
	if (ItemPointerGetOffsetNumber(&(fc_newtup->t_self)) == FirstOffsetNumber &&
		PageGetMaxOffsetNumber(fc_page) == FirstOffsetNumber)
	{
		fc_info |= XLOG_HEAP_INIT_PAGE;
		fc_init = true;
	}
	else
		fc_init = false;

	/* 准备旧页面的 WAL 数据 */
	fc_xlrec.old_offnum = ItemPointerGetOffsetNumber(&fc_oldtup->t_self);
	fc_xlrec.old_xmax = HeapTupleHeaderGetRawXmax(fc_oldtup->t_data);
	fc_xlrec.old_infobits_set = fc_compute_infobits(fc_oldtup->t_data->t_infomask,
											  fc_oldtup->t_data->t_infomask2);

	/* 准备新页面的 WAL 数据 */
	fc_xlrec.new_offnum = ItemPointerGetOffsetNumber(&fc_newtup->t_self);
	fc_xlrec.new_xmax = HeapTupleHeaderGetRawXmax(fc_newtup->t_data);

	fc_bufflags = REGBUF_STANDARD;
	if (fc_init)
		fc_bufflags |= REGBUF_WILL_INIT;
	if (fc_need_tuple_data)
		fc_bufflags |= REGBUF_KEEP_DATA;

	XLogRegisterBuffer(0, fc_newbuf, fc_bufflags);
	if (fc_oldbuf != fc_newbuf)
		XLogRegisterBuffer(1, fc_oldbuf, REGBUF_STANDARD);

	XLogRegisterData((char *) &fc_xlrec, SizeOfHeapUpdate);

	/*
	 * 准备新元组的 WAL 数据。
	 */
	if (fc_prefixlen > 0 || fc_suffixlen > 0)
	{
		if (fc_prefixlen > 0 && fc_suffixlen > 0)
		{
			fc_prefix_suffix[0] = fc_prefixlen;
			fc_prefix_suffix[1] = fc_suffixlen;
			XLogRegisterBufData(0, (char *) &fc_prefix_suffix, sizeof(uint16) * 2);
		}
		else if (fc_prefixlen > 0)
		{
			XLogRegisterBufData(0, (char *) &fc_prefixlen, sizeof(uint16));
		}
		else
		{
			XLogRegisterBufData(0, (char *) &fc_suffixlen, sizeof(uint16));
		}
	}

	fc_xlhdr.t_infomask2 = fc_newtup->t_data->t_infomask2;
	fc_xlhdr.t_infomask = fc_newtup->t_data->t_infomask;
	fc_xlhdr.t_hoff = fc_newtup->t_data->t_hoff;
	Assert(SizeofHeapTupleHeader + fc_prefixlen + fc_suffixlen <= fc_newtup->t_len);

	/*
	 * PG73FORMAT: 写入位图 [+ 填充] [+ oid] + 数据
	 *
	 * '数据'不包括公共前缀或后缀。
	 */
	XLogRegisterBufData(0, (char *) &fc_xlhdr, SizeOfHeapHeader);
	if (fc_prefixlen == 0)
	{
		XLogRegisterBufData(0,
							((char *) fc_newtup->t_data) + SizeofHeapTupleHeader,
							fc_newtup->t_len - SizeofHeapTupleHeader - fc_suffixlen);
	}
	else
	{
		/*
		 * 必须在公共前缀后将空位图和数据作为
		 * 两个独立的 rdata 条目写入。
		 */
		/* 位图 [+ 填充] [+ oid] */
		if (fc_newtup->t_data->t_hoff - SizeofHeapTupleHeader > 0)
		{
			XLogRegisterBufData(0,
								((char *) fc_newtup->t_data) + SizeofHeapTupleHeader,
								fc_newtup->t_data->t_hoff - SizeofHeapTupleHeader);
		}

		/* 公共前缀后的数据 */
		XLogRegisterBufData(0,
							((char *) fc_newtup->t_data) + fc_newtup->t_data->t_hoff + fc_prefixlen,
							fc_newtup->t_len - fc_newtup->t_data->t_hoff - fc_prefixlen - fc_suffixlen);
	}

	/* 我们需要记录一个元组身份 */
	if (fc_need_tuple_data && fc_old_key_tuple)
	{
		/* 实际上不需要这个，但解码起来更方便 */
		fc_xlhdr_idx.t_infomask2 = fc_old_key_tuple->t_data->t_infomask2;
		fc_xlhdr_idx.t_infomask = fc_old_key_tuple->t_data->t_infomask;
		fc_xlhdr_idx.t_hoff = fc_old_key_tuple->t_data->t_hoff;

		XLogRegisterData((char *) &fc_xlhdr_idx, SizeOfHeapHeader);

		/* PG73FORMAT: 写入位图 [+ 填充] [+ oid] + 数据 */
		XLogRegisterData((char *) fc_old_key_tuple->t_data + SizeofHeapTupleHeader,
						 fc_old_key_tuple->t_len - SizeofHeapTupleHeader);
	}

	/* 按行级别的来源过滤要高效得多 */
	XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);

	fc_recptr = XLogInsert(RM_HEAP_ID, fc_info);

	return fc_recptr;
}

/*
 * 执行 XLogInsert 的 XLOG_HEAP2_NEW_CID 记录
 *
 * 这仅在 wal_level >= WAL_LEVEL_LOGICAL 时使用，并且仅针对目录
 * 元组。
 */
static XLogRecPtr fc_log_heap_new_cid(Relation fc_relation, HeapTuple fc_tup)
{
	xl_heap_new_cid fc_xlrec;

	XLogRecPtr	fc_recptr;
	HeapTupleHeader fc_hdr = fc_tup->t_data;

	Assert(ItemPointerIsValid(&fc_tup->t_self));
	Assert(fc_tup->t_tableOid != InvalidOid);

	fc_xlrec.top_xid = GetTopTransactionId();
	fc_xlrec.target_node = fc_relation->rd_node;
	fc_xlrec.target_tid = fc_tup->t_self;

	/*
	 * 如果元组在同一事务中被插入和删除，我们肯定有一个
	 * 组合 CID，设置 cmin 和 cmax。
	 */
	if (fc_hdr->t_infomask & HEAP_COMBOCID)
	{
		Assert(!(fc_hdr->t_infomask & HEAP_XMAX_INVALID));
		Assert(!HeapTupleHeaderXminInvalid(fc_hdr));
		fc_xlrec.cmin = HeapTupleHeaderGetCmin(fc_hdr);
		fc_xlrec.cmax = HeapTupleHeaderGetCmax(fc_hdr);
		fc_xlrec.combocid = HeapTupleHeaderGetRawCommandId(fc_hdr);
	}
	/* 没有组合 CID，所以该事务只能设置 cmin 或 cmax */
	else
	{
		/*
		 * 元组已插入。
		 *
		 * 我们需要检查锁定仅是因为多事务可能会在
		 * FOR KEY SHARE 更新的情况下转移到新元组，
		 * 在这种情况下将会有一个 xmax，尽管元组刚刚被
		 * 插入。
		 */
		if (fc_hdr->t_infomask & HEAP_XMAX_INVALID ||
			HEAP_XMAX_IS_LOCKED_ONLY(fc_hdr->t_infomask))
		{
			fc_xlrec.cmin = HeapTupleHeaderGetRawCommandId(fc_hdr);
			fc_xlrec.cmax = InvalidCommandId;
		}
		/* 来自不同事务的元组已更新或删除。 */
		else
		{
			fc_xlrec.cmin = InvalidCommandId;
			fc_xlrec.cmax = HeapTupleHeaderGetRawCommandId(fc_hdr);
		}
		fc_xlrec.combocid = InvalidCommandId;
	}

	/*
	 * 注意，我们这里不需要注册缓冲区，因为这个
	 * 操作不会修改页面。调用我们的插入/更新/删除
	 * 确实修改了，但这是单独记录在WAL中。
	 */
	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, SizeOfHeapNewCid);

	/* 将被视为与来源无关 */

	fc_recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_NEW_CID);

	return fc_recptr;
}

/*
 * 构建一个堆元组，表示配置的REPLICA IDENTITY，以代表
 * 更新或删除中的旧元组。
 *
 * 如果不需要记录身份，或者没有定义合适的
 * 键，则返回NULL。
 *
 * 如果任何副本身份列更改了值，或者它们中有
 * 任何外部数据，则将key_required设为true。删除必须始终传递true。
 *
 * 如果返回的元组是一个修改过的副本，而不是
 * 传入的相同元组，则*copy被设置为true。
 */
static HeapTuple fc_ExtractReplicaIdentity(Relation fc_relation, HeapTuple fc_tp, bool fc_key_required,
					   bool *fc_copy)
{
	TupleDesc	fc_desc = RelationGetDescr(fc_relation);
	char		fc_replident = fc_relation->rd_rel->relreplident;
	Bitmapset  *fc_idattrs;
	HeapTuple	fc_key_tuple;
	bool		fc_nulls[MaxHeapAttributeNumber];
	Datum		fc_values[MaxHeapAttributeNumber];

	*fc_copy = false;

	if (!RelationIsLogicallyLogged(fc_relation))
		return NULL;

	if (fc_replident == REPLICA_IDENTITY_NOTHING)
		return NULL;

	if (fc_replident == REPLICA_IDENTITY_FULL)
	{
		/*
		 * 当记录整个旧元组时，它很可能包含
		 * 压缩列。如果是这样，强制它们内联。
		 */
		if (HeapTupleHasExternal(fc_tp))
		{
			*fc_copy = true;
			fc_tp = toast_flatten_tuple(fc_tp, fc_desc);
		}
		return fc_tp;
	}

	/* 如果不需要键并且我们仅记录键，我们完成了 */
	if (!fc_key_required)
		return NULL;

	/* 找出副本身份列 */
	fc_idattrs = RelationGetIndexAttrBitmap(fc_relation,
										 INDEX_ATTR_BITMAP_IDENTITY_KEY);

	/*
	 * 如果没有定义副本身份列，视为！key_required。
	 * （这个情况不应该从heap_update到达，因为那应该
	 * 准确计算key_required。但是heap_delete仅为
	 * key_required传递常量true，因此我们可以在删除中遇到这种情况。）
	 */
	if (bms_is_empty(fc_idattrs))
		return NULL;

	/*
	 * 构造一个仅包含副本身份列的新元组，
	 * 其他地方为null。在此过程中，断言副本
	 * 身份列不为null。
	 */
	heap_deform_tuple(fc_tp, fc_desc, fc_values, fc_nulls);

	for (int fc_i = 0; fc_i < fc_desc->natts; fc_i++)
	{
		if (bms_is_member(fc_i + 1 - FirstLowInvalidHeapAttributeNumber,
						  fc_idattrs))
			Assert(!fc_nulls[fc_i]);
		else
			fc_nulls[fc_i] = true;
	}

	fc_key_tuple = heap_form_tuple(fc_desc, fc_values, fc_nulls);
	*fc_copy = true;

	bms_free(fc_idattrs);

	/*
	 * 如果元组此时仅包含索引列，但仍然有
	 * 压缩列，则强制它们内联。这种情况不太可能
	 * ，因为索引列的大小有限，因此我们没有
	 * 在上面的索引列循环中重复toast_flatten_tuple()的功能，
	 * 即使这会更有效。
	 */
	if (HeapTupleHasExternal(fc_key_tuple))
	{
		HeapTuple	fc_oldtup = fc_key_tuple;

		fc_key_tuple = toast_flatten_tuple(fc_oldtup, fc_desc);
		heap_freetuple(fc_oldtup);
	}

	return fc_key_tuple;
}

/*
 * 处理XLOG_HEAP2_PRUNE记录类型。
 *
 * 获取一个完整的清理锁。
 */
static void fc_heap_xlog_prune(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_prune *fc_xlrec = (xl_heap_prune *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	RelFileNode fc_rnode;
	BlockNumber fc_blkno;
	XLogRedoAction fc_action;

	XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, &fc_blkno);

	/*
	 * 我们即将删除元组。在热备用模式下，
	 * 确保没有查询正在运行，已删除的元组仍然可见。
	 */
	if (InHotStandby)
		ResolveRecoveryConflictWithSnapshot(fc_xlrec->latestRemovedXid, fc_rnode);

	/*
	 * 如果我们有一个完整的页面映像，恢复它（使用
	 * 清理锁），然后我们完成了。
	 */
	fc_action = XLogReadBufferForRedoExtended(fc_record, 0, RBM_NORMAL, true,
										   &fc_buffer);
	if (fc_action == BLK_NEEDS_REDO)
	{
		Page		fc_page = (Page) BufferGetPage(fc_buffer);
		OffsetNumber *fc_end;
		OffsetNumber *fc_redirected;
		OffsetNumber *fc_nowdead;
		OffsetNumber *fc_nowunused;
		int			fc_nredirected;
		int			fc_ndead;
		int			fc_nunused;
		Size		fc_datalen;

		fc_redirected = (OffsetNumber *) XLogRecGetBlockData(fc_record, 0, &fc_datalen);

		fc_nredirected = fc_xlrec->nredirected;
		fc_ndead = fc_xlrec->ndead;
		fc_end = (OffsetNumber *) ((char *) fc_redirected + fc_datalen);
		fc_nowdead = fc_redirected + (fc_nredirected * 2);
		fc_nowunused = fc_nowdead + fc_ndead;
		fc_nunused = (fc_end - fc_nowunused);
		Assert(fc_nunused >= 0);

		/* 根据记录更新所有行指针，并修复碎片 */
		heap_page_prune_execute(fc_buffer,
								fc_redirected, fc_nredirected,
								fc_nowdead, fc_ndead,
								fc_nowunused, fc_nunused);

		/*
		 * 注意：我们不担心更新页面的可修剪提示。
		 * 最糟糕的情况是这将导致额外的修剪周期尽快发生。
		 */

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}

	if (BufferIsValid(fc_buffer))
	{
		Size		fc_freespace = PageGetHeapFreeSpace(BufferGetPage(fc_buffer));

		UnlockReleaseBuffer(fc_buffer);

		/*
		 * 从页面修剪记录后，更新FSM是有用的，
		 * 因为这可能导致页面成为稍后插入的目标，
		 * 即使vacuum决定不访问它（如果被标记为
		 * 全可见，则是可能的）。
		 *
		 * 无论是否应用完整页面映像，都要执行此操作，
		 * 因为FSM数据不在页面中。
		 */
		XLogRecordPageWithFreeSpace(fc_rnode, fc_blkno, fc_freespace);
	}
}

/*
 * 处理XLOG_HEAP2_VACUUM记录类型。
 *
 * 仅获取一个普通的独占锁。
 */
static void fc_heap_xlog_vacuum(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_vacuum *fc_xlrec = (xl_heap_vacuum *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	BlockNumber fc_blkno;
	XLogRedoAction fc_action;

	/*
	 * 如果我们有一个完整页面图像，恢复它（不使用清理锁）
	 * 然后我们完成了。
	 */
	fc_action = XLogReadBufferForRedoExtended(fc_record, 0, RBM_NORMAL, false,
										   &fc_buffer);
	if (fc_action == BLK_NEEDS_REDO)
	{
		Page		fc_page = (Page) BufferGetPage(fc_buffer);
		OffsetNumber *fc_nowunused;
		Size		fc_datalen;
		OffsetNumber *fc_offnum;

		fc_nowunused = (OffsetNumber *) XLogRecGetBlockData(fc_record, 0, &fc_datalen);

		/* 除非有事情要做，否则不应该是记录 */
		Assert(fc_xlrec->nunused > 0);

		/* 更新所有当前未使用的行指针 */
		fc_offnum = fc_nowunused;
		for (int fc_i = 0; fc_i < fc_xlrec->nunused; fc_i++)
		{
			OffsetNumber fc_off = *fc_offnum++;
			ItemId		fc_lp = PageGetItemId(fc_page, fc_off);

			Assert(ItemIdIsDead(fc_lp) && !ItemIdHasStorage(fc_lp));
			ItemIdSetUnused(fc_lp);
		}

		/* 现在尝试截断行指针数组 */
		PageTruncateLinePointerArray(fc_page);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}

	if (BufferIsValid(fc_buffer))
	{
		Size		fc_freespace = PageGetHeapFreeSpace(BufferGetPage(fc_buffer));
		RelFileNode fc_rnode;

		XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, &fc_blkno);

		UnlockReleaseBuffer(fc_buffer);

		/*
		 * 在从页面中清除 LP_DEAD 项目后，更新 FSM 是有用的，
		 * 因为这可能导致页面后续成为插入的目标，即使清理决定不访问它（如果
		 * 被标记为全可见则是可能的）。
		 *
		 * 此操作无论是否应用了完整页面图像都要执行，因为 FSM 数据
		 * 无论如何不在页面中。
		 */
		XLogRecordPageWithFreeSpace(fc_rnode, fc_blkno, fc_freespace);
	}
}

/*
 * 重放 XLOG_HEAP2_VISIBLE 记录。
 *
 * 这里的关键完整性要求是，我们绝不能遇到
 * 可见性映射位被设置，而页面级
 * PD_ALL_VISIBLE 位被清除的情况。如果发生这种情况，
 * 随后的页面修改将无法清除可见性映射位。
 */
static void fc_heap_xlog_visible(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_visible *fc_xlrec = (xl_heap_visible *) XLogRecGetData(fc_record);
	Buffer		fc_vmbuffer = InvalidBuffer;
	Buffer		fc_buffer;
	Page		fc_page;
	RelFileNode fc_rnode;
	BlockNumber fc_blkno;
	XLogRedoAction fc_action;

	XLogRecGetBlockTag(fc_record, 1, &fc_rnode, NULL, &fc_blkno);

	/*
	 * 如果存在任何正在运行的热备用事务，其 xmin
	 * 时间线足够久，以至于对于它们来说该页面不是全可见的，
	 * 它们可能错误地决定可以跳过堆提取进行索引仅扫描。
	 *
	 * 注意：在这里抛出某种“软”冲突可能更好，以
	 * 强制所有在飞的索引仅扫描执行堆提取，
	 * 而不是彻底杀死事务。
	 */
	if (InHotStandby)
		ResolveRecoveryConflictWithSnapshot(fc_xlrec->cutoff_xid, fc_rnode);

	/*
	 * 读取堆页面，如果它仍然存在。如果堆文件在恢复过程中已删除或
	 * 截断，则我们不需要更新页面，但我们仍然最好更新
	 * 可见性映射。
	 */
	fc_action = XLogReadBufferForRedo(fc_record, 1, &fc_buffer);
	if (fc_action == BLK_NEEDS_REDO)
	{
		/*
		 * 设置可见性映射位时，我们不会增加堆页面的 LSN
		 * （除非启用了校验和或 wal_hint_bits，此时我们必须这样做）。
		 * 这使我们面临破损页面的风险，但由于
		 * 我们以任何方式检查现有页面内容，我们并不关心。
		 *
		 * 然而，所有清除可见性映射位的操作确实会增加
		 * LSN，并且这些操作只有在 XLOG LSN
		 * 跟随页面 LSN 时才会被重放。因此，如果页面 LSN 先进于我们的
		 * XLOG 记录的 LSN，我们绝不能将页面标记为全可见，因为
		 * 随后的更新将不会被重放以清除标记。
		 */
		fc_page = BufferGetPage(fc_buffer);

		PageSetAllVisible(fc_page);

		if (XLogHintBitIsNeeded())
			PageSetLSN(fc_page, fc_lsn);

		MarkBufferDirty(fc_buffer);
	}
	else if (fc_action == BLK_RESTORED)
	{
		/*
		 * 如果堆块已备份，我们已经恢复了它，并且没有更多工作。
		 * （这只能在启用了校验和或 wal_log_hints 的情况下发生。）
		 */
	}

	if (BufferIsValid(fc_buffer))
	{
		Size		fc_space = PageGetFreeSpace(BufferGetPage(fc_buffer));

		UnlockReleaseBuffer(fc_buffer);

		/*
		 * 因为FSM不是WAL记录的，并且只是通过启发式更新，
		 * 它容易在备用服务器上变得过时。如果备用服务器后来被提升
		 * 并运行VACUUM，它将跳过更新对变得完全可见（或完全冻结，具体取决于
		 * vacuum模式）的页面的单个空闲空间数字，当FreeSpaceMapVacuum
		 * 将过于乐观的空闲空间值传播到上层FSM时，这将是麻烦的；
		 * 后续的插入者尝试使用这些页面，却发现它们不可用。这样在有很多
		 * 这样的页面时会导致长时间的停滞。
		 *
		 * 通过更新FSM对正在变得完全可见或完全冻结的页面的认知来
		 * 防止这些问题。
		 *
		 * 不论是否使用了完整页面图像，都会执行此操作，因为
		 * FSM数据反正不在页面中。
		 */
		if (fc_xlrec->flags & VISIBILITYMAP_VALID_BITS)
			XLogRecordPageWithFreeSpace(fc_rnode, fc_blkno, fc_space);
	}

	/*
	 * 即使由于LSN锁定而跳过了堆页面更新，更新可见性映射仍然是安全的。
	 * 任何清除可见性映射位的WAL记录在检查页面LSN之前执行，因此
	 * 任何需要清除的位仍将被清除。
	 */
	if (XLogReadBufferForRedoExtended(fc_record, 0, RBM_ZERO_ON_ERROR, false,
									  &fc_vmbuffer) == BLK_NEEDS_REDO)
	{
		Page		fc_vmpage = BufferGetPage(fc_vmbuffer);
		Relation	fc_reln;

		/* 如果页面被读取为零，则初始化页面 */
		if (PageIsNew(fc_vmpage))
			PageInit(fc_vmpage, BLCKSZ, 0);

		/*
		 * XLogReadBufferForRedoExtended锁定了缓冲区。但
		 * visibilitymap_set将自行处理锁定。
		 */
		LockBuffer(fc_vmbuffer, BUFFER_LOCK_UNLOCK);

		fc_reln = CreateFakeRelcacheEntry(fc_rnode);
		visibilitymap_pin(fc_reln, fc_blkno, &fc_vmbuffer);

		/*
		 * 如果重放已通过此点，则不要设置该位。
		 *
		 * 在无条件执行此操作可能是安全的；如果重放已经通过
		 * 此点，那么这次我们将至少重放到之前的同样远，如果该位需要被清除，
		 * 负责执行此操作的记录应该再次被重放，并将其清除。
		 * 目前出于过度保守的考虑，我们在这里使用与堆页面相同的测试。
		 * 如果这导致某个位被丢弃，实际上也没有什么危害；下一个
		 * VACUUM将修复它。
		 */
		if (fc_lsn > PageGetLSN(fc_vmpage))
			visibilitymap_set(fc_reln, fc_blkno, InvalidBuffer, fc_lsn, fc_vmbuffer,
							  fc_xlrec->cutoff_xid, fc_xlrec->flags);

		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}
	else if (BufferIsValid(fc_vmbuffer))
		UnlockReleaseBuffer(fc_vmbuffer);
}

/*
 * 重放XLOG_HEAP2_FREEZE_PAGE记录
 */
static void fc_heap_xlog_freeze_page(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_freeze_page *fc_xlrec = (xl_heap_freeze_page *) XLogRecGetData(fc_record);
	TransactionId fc_cutoff_xid = fc_xlrec->cutoff_xid;
	Buffer		fc_buffer;
	int			fc_ntup;

	/*
	 * 在热备用模式下，确保没有查询正在运行，它们仍然将
	 * 冻结的xids视为正在运行。
	 */
	if (InHotStandby)
	{
		RelFileNode fc_rnode;
		TransactionId fc_latestRemovedXid = fc_cutoff_xid;

		TransactionIdRetreat(fc_latestRemovedXid);

		XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, NULL);
		ResolveRecoveryConflictWithSnapshot(fc_latestRemovedXid, fc_rnode);
	}

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		Page		fc_page = BufferGetPage(fc_buffer);
		xl_heap_freeze_tuple *fc_tuples;

		fc_tuples = (xl_heap_freeze_tuple *) XLogRecGetBlockData(fc_record, 0, NULL);

		/* 现在为每个冻结元组执行冻结计划 */
		for (fc_ntup = 0; fc_ntup < fc_xlrec->ntuples; fc_ntup++)
		{
			xl_heap_freeze_tuple *fc_xlrec_tp;
			ItemId		fc_lp;
			HeapTupleHeader fc_tuple;

			fc_xlrec_tp = &fc_tuples[fc_ntup];
			fc_lp = PageGetItemId(fc_page, fc_xlrec_tp->offset); /* 偏移量是基于1的 */
			fc_tuple = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

			heap_execute_freeze_tuple(fc_tuple, fc_xlrec_tp);
		}

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

/*
 * 给定来自XLog记录的"infobits"字段，为受记录影响的元组设置
 * 在给定的infomask和infomask2中正确的位。
 *
 * （这是compute_infobits的反向操作）。
 */
static void fc_fix_infomask_from_infobits(uint8 fc_infobits, uint16 *fc_infomask, uint16 *fc_infomask2)
{
	*fc_infomask &= ~(HEAP_XMAX_IS_MULTI | HEAP_XMAX_LOCK_ONLY |
				   HEAP_XMAX_KEYSHR_LOCK | HEAP_XMAX_EXCL_LOCK);
	*fc_infomask2 &= ~HEAP_KEYS_UPDATED;

	if (fc_infobits & XLHL_XMAX_IS_MULTI)
		*fc_infomask |= HEAP_XMAX_IS_MULTI;
	if (fc_infobits & XLHL_XMAX_LOCK_ONLY)
		*fc_infomask |= HEAP_XMAX_LOCK_ONLY;
	if (fc_infobits & XLHL_XMAX_EXCL_LOCK)
		*fc_infomask |= HEAP_XMAX_EXCL_LOCK;
	/* 注意HEAP_XMAX_SHR_LOCK在这里未被考虑 */
	if (fc_infobits & XLHL_XMAX_KEYSHR_LOCK)
		*fc_infomask |= HEAP_XMAX_KEYSHR_LOCK;

	if (fc_infobits & XLHL_KEYS_UPDATED)
		*fc_infomask2 |= HEAP_KEYS_UPDATED;
}

static void fc_heap_xlog_delete(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_delete *fc_xlrec = (xl_heap_delete *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;
	BlockNumber fc_blkno;
	RelFileNode fc_target_node;
	ItemPointerData fc_target_tid;

	XLogRecGetBlockTag(fc_record, 0, &fc_target_node, NULL, &fc_blkno);
	ItemPointerSetBlockNumber(&fc_target_tid, fc_blkno);
	ItemPointerSetOffsetNumber(&fc_target_tid, fc_xlrec->offnum);

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_DELETE_ALL_VISIBLE_CLEARED)
	{
		Relation	fc_reln = CreateFakeRelcacheEntry(fc_target_node);
		Buffer		fc_vmbuffer = InvalidBuffer;

		visibilitymap_pin(fc_reln, fc_blkno, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_blkno, fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		fc_page = BufferGetPage(fc_buffer);

		if (PageGetMaxOffsetNumber(fc_page) >= fc_xlrec->offnum)
			fc_lp = PageGetItemId(fc_page, fc_xlrec->offnum);

		if (PageGetMaxOffsetNumber(fc_page) < fc_xlrec->offnum || !ItemIdIsNormal(fc_lp))
			elog(PANIC, "invalid lp");

		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

		fc_htup->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
		fc_htup->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		HeapTupleHeaderClearHotUpdated(fc_htup);
		fc_fix_infomask_from_infobits(fc_xlrec->infobits_set,
								   &fc_htup->t_infomask, &fc_htup->t_infomask2);
		if (!(fc_xlrec->flags & XLH_DELETE_IS_SUPER))
			HeapTupleHeaderSetXmax(fc_htup, fc_xlrec->xmax);
		else
			HeapTupleHeaderSetXmin(fc_htup, InvalidTransactionId);
		HeapTupleHeaderSetCmax(fc_htup, FirstCommandId, false);

		/* 将页面标记为修剪候选者 */
		PageSetPrunable(fc_page, XLogRecGetXid(fc_record));

		if (fc_xlrec->flags & XLH_DELETE_ALL_VISIBLE_CLEARED)
			PageClearAllVisible(fc_page);

		/* 确保t_ctid正确设置 */
		if (fc_xlrec->flags & XLH_DELETE_IS_PARTITION_MOVE)
			HeapTupleHeaderSetMovedPartitions(fc_htup);
		else
			fc_htup->t_ctid = fc_target_tid;
		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

static void fc_heap_xlog_insert(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_insert *fc_xlrec = (xl_heap_insert *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;
	union
	{
		HeapTupleHeaderData hdr;
		char		data[MaxHeapTupleSize];
	}			fc_tbuf;
	HeapTupleHeader fc_htup;
	xl_heap_header fc_xlhdr;
	uint32		fc_newlen;
	Size		fc_freespace = 0;
	RelFileNode fc_target_node;
	BlockNumber fc_blkno;
	ItemPointerData fc_target_tid;
	XLogRedoAction fc_action;

	XLogRecGetBlockTag(fc_record, 0, &fc_target_node, NULL, &fc_blkno);
	ItemPointerSetBlockNumber(&fc_target_tid, fc_blkno);
	ItemPointerSetOffsetNumber(&fc_target_tid, fc_xlrec->offnum);

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED)
	{
		Relation	fc_reln = CreateFakeRelcacheEntry(fc_target_node);
		Buffer		fc_vmbuffer = InvalidBuffer;

		visibilitymap_pin(fc_reln, fc_blkno, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_blkno, fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	/*
	 * 如果我们在页面上插入了第一个也是唯一的元组，重新初始化页面。
	 */
	if (XLogRecGetInfo(fc_record) & XLOG_HEAP_INIT_PAGE)
	{
		fc_buffer = XLogInitBufferForRedo(fc_record, 0);
		fc_page = BufferGetPage(fc_buffer);
		PageInit(fc_page, BufferGetPageSize(fc_buffer), 0);
		fc_action = BLK_NEEDS_REDO;
	}
	else
		fc_action = XLogReadBufferForRedo(fc_record, 0, &fc_buffer);
	if (fc_action == BLK_NEEDS_REDO)
	{
		Size		fc_datalen;
		char	   *fc_data;

		fc_page = BufferGetPage(fc_buffer);

		if (PageGetMaxOffsetNumber(fc_page) + 1 < fc_xlrec->offnum)
			elog(PANIC, "invalid max offset number");

		fc_data = XLogRecGetBlockData(fc_record, 0, &fc_datalen);

		fc_newlen = fc_datalen - SizeOfHeapHeader;
		Assert(fc_datalen > SizeOfHeapHeader && fc_newlen <= MaxHeapTupleSize);
		memcpy((char *) &fc_xlhdr, fc_data, SizeOfHeapHeader);
		fc_data += SizeOfHeapHeader;

		fc_htup = &fc_tbuf.hdr;
		MemSet((char *) fc_htup, 0, SizeofHeapTupleHeader);
		/* PG73FORMAT: 获取位图 [+ 填充] [+ oid] + 数据 */
		memcpy((char *) fc_htup + SizeofHeapTupleHeader,
			   fc_data,
			   fc_newlen);
		fc_newlen += SizeofHeapTupleHeader;
		fc_htup->t_infomask2 = fc_xlhdr.t_infomask2;
		fc_htup->t_infomask = fc_xlhdr.t_infomask;
		fc_htup->t_hoff = fc_xlhdr.t_hoff;
		HeapTupleHeaderSetXmin(fc_htup, XLogRecGetXid(fc_record));
		HeapTupleHeaderSetCmin(fc_htup, FirstCommandId);
		fc_htup->t_ctid = fc_target_tid;

		if (PageAddItem(fc_page, (Item) fc_htup, fc_newlen, fc_xlrec->offnum,
						true, true) == InvalidOffsetNumber)
			elog(PANIC, "failed to add tuple");

		fc_freespace = PageGetHeapFreeSpace(fc_page); /* 需要更新下面的 FSM */

		PageSetLSN(fc_page, fc_lsn);

		if (fc_xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED)
			PageClearAllVisible(fc_page);

		/* XLH_INSERT_ALL_FROZEN_SET意味着所有元组都是可见的 */
		if (fc_xlrec->flags & XLH_INSERT_ALL_FROZEN_SET)
			PageSetAllVisible(fc_page);

		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);

	/*
	 * 如果页面可用空间不足，需更新FSM。
	 * 我们对“低”的定义是小于20%。如果不知道表的填充因子，我们无法做得更好。
	 *
	 * XXX：如果页面是从完整页面图像恢复的，就不要这样做。 在这种情况下，我们不会更新FSM，反正也不需要完全准确。
	 */
	if (fc_action == BLK_NEEDS_REDO && fc_freespace < BLCKSZ / 5)
		XLogRecordPageWithFreeSpace(fc_target_node, fc_blkno, fc_freespace);
}

/*
 * 处理MULTI_INSERT记录类型。
 */
static void fc_heap_xlog_multi_insert(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_multi_insert *fc_xlrec;
	RelFileNode fc_rnode;
	BlockNumber fc_blkno;
	Buffer		fc_buffer;
	Page		fc_page;
	union
	{
		HeapTupleHeaderData hdr;
		char		data[MaxHeapTupleSize];
	}			fc_tbuf;
	HeapTupleHeader fc_htup;
	uint32		fc_newlen;
	Size		fc_freespace = 0;
	int			fc_i;
	bool		fc_isinit = (XLogRecGetInfo(fc_record) & XLOG_HEAP_INIT_PAGE) != 0;
	XLogRedoAction fc_action;

	/*
	 * 插入不会覆盖MVCC数据，因此不需要冲突处理。
	 */
	fc_xlrec = (xl_heap_multi_insert *) XLogRecGetData(fc_record);

	XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, &fc_blkno);

	/* 检查互斥标志是否没有同时设置 */
	Assert(!((fc_xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) &&
			 (fc_xlrec->flags & XLH_INSERT_ALL_FROZEN_SET)));

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED)
	{
		Relation	fc_reln = CreateFakeRelcacheEntry(fc_rnode);
		Buffer		fc_vmbuffer = InvalidBuffer;

		visibilitymap_pin(fc_reln, fc_blkno, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_blkno, fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	if (fc_isinit)
	{
		fc_buffer = XLogInitBufferForRedo(fc_record, 0);
		fc_page = BufferGetPage(fc_buffer);
		PageInit(fc_page, BufferGetPageSize(fc_buffer), 0);
		fc_action = BLK_NEEDS_REDO;
	}
	else
		fc_action = XLogReadBufferForRedo(fc_record, 0, &fc_buffer);
	if (fc_action == BLK_NEEDS_REDO)
	{
		char	   *fc_tupdata;
		char	   *fc_endptr;
		Size		fc_len;

		/* 元组作为块数据存储 */
		fc_tupdata = XLogRecGetBlockData(fc_record, 0, &fc_len);
		fc_endptr = fc_tupdata + fc_len;

		fc_page = (Page) BufferGetPage(fc_buffer);

		for (fc_i = 0; fc_i < fc_xlrec->ntuples; fc_i++)
		{
			OffsetNumber fc_offnum;
			xl_multi_insert_tuple *fc_xlhdr;

			/*
			 * 如果我们正在重新初始化页面，元组按FirstOffsetNumber的顺序存储。否则，WAL记录中有一个偏移量数组，元组在其之后。
			 */
			if (fc_isinit)
				fc_offnum = FirstOffsetNumber + fc_i;
			else
				fc_offnum = fc_xlrec->offsets[fc_i];
			if (PageGetMaxOffsetNumber(fc_page) + 1 < fc_offnum)
				elog(PANIC, "invalid max offset number");

			fc_xlhdr = (xl_multi_insert_tuple *) SHORTALIGN(fc_tupdata);
			fc_tupdata = ((char *) fc_xlhdr) + SizeOfMultiInsertTuple;

			fc_newlen = fc_xlhdr->datalen;
			Assert(fc_newlen <= MaxHeapTupleSize);
			fc_htup = &fc_tbuf.hdr;
			MemSet((char *) fc_htup, 0, SizeofHeapTupleHeader);
			/* PG73FORMAT: 获取位图 [+ 填充] [+ oid] + 数据 */
			memcpy((char *) fc_htup + SizeofHeapTupleHeader,
				   (char *) fc_tupdata,
				   fc_newlen);
			fc_tupdata += fc_newlen;

			fc_newlen += SizeofHeapTupleHeader;
			fc_htup->t_infomask2 = fc_xlhdr->t_infomask2;
			fc_htup->t_infomask = fc_xlhdr->t_infomask;
			fc_htup->t_hoff = fc_xlhdr->t_hoff;
			HeapTupleHeaderSetXmin(fc_htup, XLogRecGetXid(fc_record));
			HeapTupleHeaderSetCmin(fc_htup, FirstCommandId);
			ItemPointerSetBlockNumber(&fc_htup->t_ctid, fc_blkno);
			ItemPointerSetOffsetNumber(&fc_htup->t_ctid, fc_offnum);

			fc_offnum = PageAddItem(fc_page, (Item) fc_htup, fc_newlen, fc_offnum, true, true);
			if (fc_offnum == InvalidOffsetNumber)
				elog(PANIC, "failed to add tuple");
		}
		if (fc_tupdata != fc_endptr)
			elog(PANIC, "total tuple length mismatch");

		fc_freespace = PageGetHeapFreeSpace(fc_page); /* 需要更新下面的 FSM */

		PageSetLSN(fc_page, fc_lsn);

		if (fc_xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED)
			PageClearAllVisible(fc_page);

		/* XLH_INSERT_ALL_FROZEN_SET意味着所有元组都是可见的 */
		if (fc_xlrec->flags & XLH_INSERT_ALL_FROZEN_SET)
			PageSetAllVisible(fc_page);

		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);

	/*
	 * 如果页面可用空间不足，需更新FSM。
	 * 我们对“低”的定义是小于20%。如果不知道表的填充因子，我们无法做得更好。
	 *
	 * XXX：如果页面是从完整页面图像恢复的，就不要这样做。 在这种情况下，我们不会更新FSM，反正也不需要完全准确。
	 */
	if (fc_action == BLK_NEEDS_REDO && fc_freespace < BLCKSZ / 5)
		XLogRecordPageWithFreeSpace(fc_rnode, fc_blkno, fc_freespace);
}

/*
 * 处理UPDATE和HOT_UPDATE
 */
static void fc_heap_xlog_update(XLogReaderState *fc_record, bool fc_hot_update)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_update *fc_xlrec = (xl_heap_update *) XLogRecGetData(fc_record);
	RelFileNode fc_rnode;
	BlockNumber fc_oldblk;
	BlockNumber fc_newblk;
	ItemPointerData fc_newtid;
	Buffer		fc_obuffer,
				fc_nbuffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleData fc_oldtup;
	HeapTupleHeader fc_htup;
	uint16		fc_prefixlen = 0,
				fc_suffixlen = 0;
	char	   *fc_newp;
	union
	{
		HeapTupleHeaderData hdr;
		char		data[MaxHeapTupleSize];
	}			fc_tbuf;
	xl_heap_header fc_xlhdr;
	uint32		fc_newlen;
	Size		fc_freespace = 0;
	XLogRedoAction fc_oldaction;
	XLogRedoAction fc_newaction;

	/* 初始化以保持编译器安静 */
	fc_oldtup.t_data = NULL;
	fc_oldtup.t_len = 0;

	XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, &fc_newblk);
	if (XLogRecGetBlockTagExtended(fc_record, 1, NULL, NULL, &fc_oldblk, NULL))
	{
		/* HOT更新不会跨页面进行 */
		Assert(!fc_hot_update);
	}
	else
		fc_oldblk = fc_newblk;

	ItemPointerSet(&fc_newtid, fc_newblk, fc_xlrec->new_offnum);

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED)
	{
		Relation	fc_reln = CreateFakeRelcacheEntry(fc_rnode);
		Buffer		fc_vmbuffer = InvalidBuffer;

		visibilitymap_pin(fc_reln, fc_oldblk, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_oldblk, fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	/*
	 * 在正常操作中，重要的是按照页面编号顺序锁定两个页面，以避免与其他更新操作的可能死锁。 然而，在WAL重放期间，不会发生其他更新，因此我们不需要担心这一点。但我们*确实*需要担心不向热备份查询暴露不一致的状态——因此在添加新元组到新页面之前，原始页面不能被解锁。
	 */

	/* 处理旧元组版本 */
	fc_oldaction = XLogReadBufferForRedo(fc_record, (fc_oldblk == fc_newblk) ? 0 : 1,
									  &fc_obuffer);
	if (fc_oldaction == BLK_NEEDS_REDO)
	{
		fc_page = BufferGetPage(fc_obuffer);
		fc_offnum = fc_xlrec->old_offnum;
		if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
			fc_lp = PageGetItemId(fc_page, fc_offnum);

		if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
			elog(PANIC, "invalid lp");

		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

		fc_oldtup.t_data = fc_htup;
		fc_oldtup.t_len = ItemIdGetLength(fc_lp);

		fc_htup->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
		fc_htup->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		if (fc_hot_update)
			HeapTupleHeaderSetHotUpdated(fc_htup);
		else
			HeapTupleHeaderClearHotUpdated(fc_htup);
		fc_fix_infomask_from_infobits(fc_xlrec->old_infobits_set, &fc_htup->t_infomask,
								   &fc_htup->t_infomask2);
		HeapTupleHeaderSetXmax(fc_htup, fc_xlrec->old_xmax);
		HeapTupleHeaderSetCmax(fc_htup, FirstCommandId, false);
		/* 在t_ctid中设置向前链链接 */
		fc_htup->t_ctid = fc_newtid;

		/* 将页面标记为修剪候选者 */
		PageSetPrunable(fc_page, XLogRecGetXid(fc_record));

		if (fc_xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED)
			PageClearAllVisible(fc_page);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_obuffer);
	}

	/*
	 * 读取新元组要放入的页面，如果与旧的不同。
	 */
	if (fc_oldblk == fc_newblk)
	{
		fc_nbuffer = fc_obuffer;
		fc_newaction = fc_oldaction;
	}
	else if (XLogRecGetInfo(fc_record) & XLOG_HEAP_INIT_PAGE)
	{
		fc_nbuffer = XLogInitBufferForRedo(fc_record, 0);
		fc_page = (Page) BufferGetPage(fc_nbuffer);
		PageInit(fc_page, BufferGetPageSize(fc_nbuffer), 0);
		fc_newaction = BLK_NEEDS_REDO;
	}
	else
		fc_newaction = XLogReadBufferForRedo(fc_record, 0, &fc_nbuffer);

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED)
	{
		Relation	fc_reln = CreateFakeRelcacheEntry(fc_rnode);
		Buffer		fc_vmbuffer = InvalidBuffer;

		visibilitymap_pin(fc_reln, fc_newblk, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_newblk, fc_vmbuffer, VISIBILITYMAP_VALID_BITS);
		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	/* 处理新元组 */
	if (fc_newaction == BLK_NEEDS_REDO)
	{
		char	   *fc_recdata;
		char	   *fc_recdata_end;
		Size		fc_datalen;
		Size		fc_tuplen;

		fc_recdata = XLogRecGetBlockData(fc_record, 0, &fc_datalen);
		fc_recdata_end = fc_recdata + fc_datalen;

		fc_page = BufferGetPage(fc_nbuffer);

		fc_offnum = fc_xlrec->new_offnum;
		if (PageGetMaxOffsetNumber(fc_page) + 1 < fc_offnum)
			elog(PANIC, "invalid max offset number");

		if (fc_xlrec->flags & XLH_UPDATE_PREFIX_FROM_OLD)
		{
			Assert(fc_newblk == fc_oldblk);
			memcpy(&fc_prefixlen, fc_recdata, sizeof(uint16));
			fc_recdata += sizeof(uint16);
		}
		if (fc_xlrec->flags & XLH_UPDATE_SUFFIX_FROM_OLD)
		{
			Assert(fc_newblk == fc_oldblk);
			memcpy(&fc_suffixlen, fc_recdata, sizeof(uint16));
			fc_recdata += sizeof(uint16);
		}

		memcpy((char *) &fc_xlhdr, fc_recdata, SizeOfHeapHeader);
		fc_recdata += SizeOfHeapHeader;

		fc_tuplen = fc_recdata_end - fc_recdata;
		Assert(fc_tuplen <= MaxHeapTupleSize);

		fc_htup = &fc_tbuf.hdr;
		MemSet((char *) fc_htup, 0, SizeofHeapTupleHeader);

		/*
		 * 使用旧元组的前缀和/或后缀，以及存储在WAL记录中的数据重建新元组。
		 */
		fc_newp = (char *) fc_htup + SizeofHeapTupleHeader;
		if (fc_prefixlen > 0)
		{
			int			fc_len;

			/* 从WAL记录复制位图 [+ 填充] [+ oid] */
			fc_len = fc_xlhdr.t_hoff - SizeofHeapTupleHeader;
			memcpy(fc_newp, fc_recdata, fc_len);
			fc_recdata += fc_len;
			fc_newp += fc_len;

			/* 从旧元组复制前缀 */
			memcpy(fc_newp, (char *) fc_oldtup.t_data + fc_oldtup.t_data->t_hoff, fc_prefixlen);
			fc_newp += fc_prefixlen;

			/* 从WAL记录复制新元组数据 */
			fc_len = fc_tuplen - (fc_xlhdr.t_hoff - SizeofHeapTupleHeader);
			memcpy(fc_newp, fc_recdata, fc_len);
			fc_recdata += fc_len;
			fc_newp += fc_len;
		}
		else
		{
			/*
			 * 复制位图 [+ 填充] [+ oid] + 数据从记录中，一次性完成
			 */
			memcpy(fc_newp, fc_recdata, fc_tuplen);
			fc_recdata += fc_tuplen;
			fc_newp += fc_tuplen;
		}
		Assert(fc_recdata == fc_recdata_end);

		/* 从旧元组复制后缀 */
		if (fc_suffixlen > 0)
			memcpy(fc_newp, (char *) fc_oldtup.t_data + fc_oldtup.t_len - fc_suffixlen, fc_suffixlen);

		fc_newlen = SizeofHeapTupleHeader + fc_tuplen + fc_prefixlen + fc_suffixlen;
		fc_htup->t_infomask2 = fc_xlhdr.t_infomask2;
		fc_htup->t_infomask = fc_xlhdr.t_infomask;
		fc_htup->t_hoff = fc_xlhdr.t_hoff;

		HeapTupleHeaderSetXmin(fc_htup, XLogRecGetXid(fc_record));
		HeapTupleHeaderSetCmin(fc_htup, FirstCommandId);
		HeapTupleHeaderSetXmax(fc_htup, fc_xlrec->new_xmax);
		/* 确保 t_ctid 中没有向前链路 */
		fc_htup->t_ctid = fc_newtid;

		fc_offnum = PageAddItem(fc_page, (Item) fc_htup, fc_newlen, fc_offnum, true, true);
		if (fc_offnum == InvalidOffsetNumber)
			elog(PANIC, "failed to add tuple");

		if (fc_xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED)
			PageClearAllVisible(fc_page);

		fc_freespace = PageGetHeapFreeSpace(fc_page); /* 需要更新下面的 FSM */

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_nbuffer);
	}

	if (BufferIsValid(fc_nbuffer) && fc_nbuffer != fc_obuffer)
		UnlockReleaseBuffer(fc_nbuffer);
	if (BufferIsValid(fc_obuffer))
		UnlockReleaseBuffer(fc_obuffer);

	/*
	 * 如果新页面的可用空间不足，亦需更新 FSM。
	 * 随意定义 "不足" 为小于 20%。如果不知道表的填充因子，无法做得更好。
	 *
	 * 然而，HOT 更新时不要更新 FSM，因为在崩溃恢复后，旧元组或新元组肯定会是死的并且可修剪。
	 * 修剪后，页面的可用空间大致与更新前相同，前提是新元组的大小与旧元组大致相同。
	 *
	 * XXX: 如果页面是从完整页面图像恢复的，则不要执行此操作。在这种情况下，我们不更新 FSM，反正不需要完全准确。
	 */
	if (fc_newaction == BLK_NEEDS_REDO && !fc_hot_update && fc_freespace < BLCKSZ / 5)
		XLogRecordPageWithFreeSpace(fc_rnode, fc_newblk, fc_freespace);
}

static void fc_heap_xlog_confirm(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_confirm *fc_xlrec = (xl_heap_confirm *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		fc_page = BufferGetPage(fc_buffer);

		fc_offnum = fc_xlrec->offnum;
		if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
			fc_lp = PageGetItemId(fc_page, fc_offnum);

		if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
			elog(PANIC, "invalid lp");

		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

		/*
		 * 确认元组实际已插入
		 */
		ItemPointerSet(&fc_htup->t_ctid, BufferGetBlockNumber(fc_buffer), fc_offnum);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

static void fc_heap_xlog_lock(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_lock *fc_xlrec = (xl_heap_lock *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED)
	{
		RelFileNode fc_rnode;
		Buffer		fc_vmbuffer = InvalidBuffer;
		BlockNumber fc_block;
		Relation	fc_reln;

		XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, &fc_block);
		fc_reln = CreateFakeRelcacheEntry(fc_rnode);

		visibilitymap_pin(fc_reln, fc_block, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_block, fc_vmbuffer, VISIBILITYMAP_ALL_FROZEN);

		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		fc_page = (Page) BufferGetPage(fc_buffer);

		fc_offnum = fc_xlrec->offnum;
		if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
			fc_lp = PageGetItemId(fc_page, fc_offnum);

		if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
			elog(PANIC, "invalid lp");

		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

		fc_htup->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
		fc_htup->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		fc_fix_infomask_from_infobits(fc_xlrec->infobits_set, &fc_htup->t_infomask,
								   &fc_htup->t_infomask2);

		/*
		 * 清除相关更新标志，但仅当修改的 infomask 表示没有更新时。
		 */
		if (HEAP_XMAX_IS_LOCKED_ONLY(fc_htup->t_infomask))
		{
			HeapTupleHeaderClearHotUpdated(fc_htup);
			/* 确保 t_ctid 中没有向前链路 */
			ItemPointerSet(&fc_htup->t_ctid,
						   BufferGetBlockNumber(fc_buffer),
						   fc_offnum);
		}
		HeapTupleHeaderSetXmax(fc_htup, fc_xlrec->locking_xid);
		HeapTupleHeaderSetCmax(fc_htup, FirstCommandId, false);
		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

static void fc_heap_xlog_lock_updated(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_lock_updated *fc_xlrec;
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;

	fc_xlrec = (xl_heap_lock_updated *) XLogRecGetData(fc_record);

	/*
	 * 即使堆页面已经是最新的，可见性映射也可能需要修复。
	 */
	if (fc_xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED)
	{
		RelFileNode fc_rnode;
		Buffer		fc_vmbuffer = InvalidBuffer;
		BlockNumber fc_block;
		Relation	fc_reln;

		XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, &fc_block);
		fc_reln = CreateFakeRelcacheEntry(fc_rnode);

		visibilitymap_pin(fc_reln, fc_block, &fc_vmbuffer);
		visibilitymap_clear(fc_reln, fc_block, fc_vmbuffer, VISIBILITYMAP_ALL_FROZEN);

		ReleaseBuffer(fc_vmbuffer);
		FreeFakeRelcacheEntry(fc_reln);
	}

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		fc_page = BufferGetPage(fc_buffer);

		fc_offnum = fc_xlrec->offnum;
		if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
			fc_lp = PageGetItemId(fc_page, fc_offnum);

		if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
			elog(PANIC, "invalid lp");

		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

		fc_htup->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED);
		fc_htup->t_infomask2 &= ~HEAP_KEYS_UPDATED;
		fc_fix_infomask_from_infobits(fc_xlrec->infobits_set, &fc_htup->t_infomask,
								   &fc_htup->t_infomask2);
		HeapTupleHeaderSetXmax(fc_htup, fc_xlrec->xmax);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

static void fc_heap_xlog_inplace(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	xl_heap_inplace *fc_xlrec = (xl_heap_inplace *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;
	OffsetNumber fc_offnum;
	ItemId		fc_lp = NULL;
	HeapTupleHeader fc_htup;
	uint32		fc_oldlen;
	Size		fc_newlen;

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		char	   *fc_newtup = XLogRecGetBlockData(fc_record, 0, &fc_newlen);

		fc_page = BufferGetPage(fc_buffer);

		fc_offnum = fc_xlrec->offnum;
		if (PageGetMaxOffsetNumber(fc_page) >= fc_offnum)
			fc_lp = PageGetItemId(fc_page, fc_offnum);

		if (PageGetMaxOffsetNumber(fc_page) < fc_offnum || !ItemIdIsNormal(fc_lp))
			elog(PANIC, "invalid lp");

		fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_lp);

		fc_oldlen = ItemIdGetLength(fc_lp) - fc_htup->t_hoff;
		if (fc_oldlen != fc_newlen)
			elog(PANIC, "wrong tuple length");

		memcpy((char *) fc_htup + fc_htup->t_hoff, fc_newtup, fc_newlen);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

void heap_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;

	/*
	 * 这些操作不会覆盖 MVCC 数据，因此不需要冲突处理。
	 * heap2 rmgr 中的操作会。
	 */

	switch (fc_info & XLOG_HEAP_OPMASK)
	{
		case XLOG_HEAP_INSERT:
			fc_heap_xlog_insert(fc_record);
			break;
		case XLOG_HEAP_DELETE:
			fc_heap_xlog_delete(fc_record);
			break;
		case XLOG_HEAP_UPDATE:
			fc_heap_xlog_update(fc_record, false);
			break;
		case XLOG_HEAP_TRUNCATE:

			/*
			 * TRUNCATE 是一个无操作，因为这些操作已经记录为
			 * SMGR WAL 记录。 TRUNCATE WAL 记录仅存在于逻辑解码中。
			 */
			break;
		case XLOG_HEAP_HOT_UPDATE:
			fc_heap_xlog_update(fc_record, true);
			break;
		case XLOG_HEAP_CONFIRM:
			fc_heap_xlog_confirm(fc_record);
			break;
		case XLOG_HEAP_LOCK:
			fc_heap_xlog_lock(fc_record);
			break;
		case XLOG_HEAP_INPLACE:
			fc_heap_xlog_inplace(fc_record);
			break;
		default:
			elog(PANIC, "heap_redo: unknown op code %u", fc_info);
	}
}

void heap2_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;

	switch (fc_info & XLOG_HEAP_OPMASK)
	{
		case XLOG_HEAP2_PRUNE:
			fc_heap_xlog_prune(fc_record);
			break;
		case XLOG_HEAP2_VACUUM:
			fc_heap_xlog_vacuum(fc_record);
			break;
		case XLOG_HEAP2_FREEZE_PAGE:
			fc_heap_xlog_freeze_page(fc_record);
			break;
		case XLOG_HEAP2_VISIBLE:
			fc_heap_xlog_visible(fc_record);
			break;
		case XLOG_HEAP2_MULTI_INSERT:
			fc_heap_xlog_multi_insert(fc_record);
			break;
		case XLOG_HEAP2_LOCK_UPDATED:
			fc_heap_xlog_lock_updated(fc_record);
			break;
		case XLOG_HEAP2_NEW_CID:

			/*
			 * 在真实重放中无需执行任何操作，仅在逻辑解码期间使用。
			 */
			break;
		case XLOG_HEAP2_REWRITE:
			heap_xlog_logical_rewrite(fc_record);
			break;
		default:
			elog(PANIC, "heap2_redo: unknown op code %u", fc_info);
	}
}

/*
 * 在对堆页面执行一致性检查之前进行掩码处理。
 */
void heap_mask(char *fc_pagedata, BlockNumber fc_blkno)
{
	Page		fc_page = (Page) fc_pagedata;
	OffsetNumber fc_off;

	mask_page_lsn_and_checksum(fc_page);

	mask_page_hint_bits(fc_page);
	mask_unused_space(fc_page);

	for (fc_off = 1; fc_off <= PageGetMaxOffsetNumber(fc_page); fc_off++)
	{
		ItemId		fc_iid = PageGetItemId(fc_page, fc_off);
		char	   *fc_page_item;

		fc_page_item = (char *) (fc_page + ItemIdGetOffset(fc_iid));

		if (ItemIdIsNormal(fc_iid))
		{
			HeapTupleHeader fc_page_htup = (HeapTupleHeader) fc_page_item;

			/*
			 * 如果元组的 xmin 尚未被冻结，我们应该忽略提示位的差异，
			 * 因为它们可以在不发出 WAL 的情况下设置。
			 */
			if (!HeapTupleHeaderXminFrozen(fc_page_htup))
				fc_page_htup->t_infomask &= ~HEAP_XACT_MASK;
			else
			{
				/* 我们仍需掩码 xmax 提示位。 */
				fc_page_htup->t_infomask &= ~HEAP_XMAX_INVALID;
				fc_page_htup->t_infomask &= ~HEAP_XMAX_COMMITTED;
			}

			/*
			 * 在重放期间，我们将命令 ID 设置为 FirstCommandId。因此，掩码它。
			 * 详见 heap_xlog_insert()。
			 */
			fc_page_htup->t_choice.t_heap.t_field3.t_cid = MASK_MARKER;

			/*
			 * 对于投机元组，heap_insert() 不在调用者传递的堆元组本身中设置 ctid，
			 * 而是将 ctid 字段留空以容纳一个投机令牌值——一个每个后端单调递增的标识符。
			 * 此外，任何情况下都不记录 ctid 的 WAL。
			 *
			 * 在重做期间，heap_xlog_insert() 将 t_ctid 设置为当前块号和自身偏移号。
			 * 它不关心主节点上的任何投机插入。因此，我们将 t_ctid 设置为当前块号和自身偏移号，以忽略任何不一致性。
			 */
			if (HeapTupleHeaderIsSpeculative(fc_page_htup))
				ItemPointerSet(&fc_page_htup->t_ctid, fc_blkno, fc_off);

			/*
			 * 注意：由于元组位置发生变化而导致的ctid变化不被忽略
			 * （即HeapTupleHeaderIndicatesMovedPartitions），因为这是
			 * 重要的信息，需要在主节点和备用节点之间保持同步，因此被
			 * 记录在WAL中。
			 */
		}

		/*
		 * 当项的长度未对齐到MAXALIGN时，忽略元组后的任何填充字节。
		 */
		if (ItemIdHasStorage(fc_iid))
		{
			int			fc_len = ItemIdGetLength(fc_iid);
			int			fc_padlen = MAXALIGN(fc_len) - fc_len;

			if (fc_padlen > 0)
				memset(fc_page_item + fc_len, MASK_MARKER, fc_padlen);
		}
	}
}

/*
 * HeapCheckForSerializableConflictOut
 *		我们正在读取一个元组。如果它不可见，可能与插入者
 *		存在读写冲突。否则，如果我们可见但已被删除，可能与删除者
 *		存在读写冲突。
 *
 * 我们将确定可能与之存在冲突的写入事务的顶级xid，并请求
 * CheckForSerializableConflictOut()来检查与我们自己的事务是否
 * 存在重叠。
 *
 * 该函数应在heapam.c中的几乎任何读取元组的地方调用。调用者
 * 必须至少对缓冲区保持共享锁，因为此函数可能会在元组上设置
 * 提示位。目前没有已知原因从索引AM调用此函数。
 */
void HeapCheckForSerializableConflictOut(bool fc_visible, Relation fc_relation,
									HeapTuple fc_tuple, Buffer fc_buffer,
									Snapshot fc_snapshot)
{
	TransactionId fc_xid;
	HTSV_Result fc_htsvResult;

	if (!CheckForSerializableConflictOutNeeded(fc_relation, fc_snapshot))
		return;

	/*
	 * 检查元组是否被并发事务写入，导致它对我们不可见，
	 * 或在它对我们可见时被删除。“visible”布尔值表示元组
	 * 是否对我们可见，而HeapTupleSatisfiesVacuum检查
	 * 该元组的其他状态。
	 *
	 * 如果有一个同时插入的元组也恰好被（另一个事务）同时更新，
	 * 将使用元组的xmin，而不是更新者的xid。
	 */
	fc_htsvResult = HeapTupleSatisfiesVacuum(fc_tuple, TransactionXmin, fc_buffer);
	switch (fc_htsvResult)
	{
		case HEAPTUPLE_LIVE:
			if (fc_visible)
				return;
			fc_xid = HeapTupleHeaderGetXmin(fc_tuple->t_data);
			break;
		case HEAPTUPLE_RECENTLY_DEAD:
		case HEAPTUPLE_DELETE_IN_PROGRESS:
			if (fc_visible)
				fc_xid = HeapTupleHeaderGetUpdateXid(fc_tuple->t_data);
			else
				fc_xid = HeapTupleHeaderGetXmin(fc_tuple->t_data);

			if (TransactionIdPrecedes(fc_xid, TransactionXmin))
			{
				/* 这类似于HEAPTUPLE_DEAD情况 */
				Assert(!fc_visible);
				return;
			}
			break;
		case HEAPTUPLE_INSERT_IN_PROGRESS:
			fc_xid = HeapTupleHeaderGetXmin(fc_tuple->t_data);
			break;
		case HEAPTUPLE_DEAD:
			Assert(!fc_visible);
			return;
		default:

			/*
			 * 只有在没有将新值添加到此switch语句的情况下，
			 * 才能到达此默认子句。这是一个错误，因此记录日志。
			 */
			elog(ERROR, "unrecognized return value from HeapTupleSatisfiesVacuum: %u", fc_htsvResult);

			/*
			 * 尽管所有枚举值都已覆盖并在默认情况下调用elog，一些编译器仍
			 * 认为这是一个代码路径，允许在未初始化的情况下使用xid。静默
			 * 该警告。
			 */
			fc_xid = InvalidTransactionId;
	}

	Assert(TransactionIdIsValid(fc_xid));
	Assert(TransactionIdFollowsOrEquals(fc_xid, TransactionXmin));

	/*
	 * 查找顶级xid。如果xid过早，无法成为冲突，或者如果是
	 * 我们自己的xid，则退出。
	 */
	if (TransactionIdEquals(fc_xid, GetTopTransactionIdIfAny()))
		return;
	fc_xid = SubTransGetTopmostTransaction(fc_xid);
	if (TransactionIdPrecedes(fc_xid, TransactionXmin))
		return;

	CheckForSerializableConflictOut(fc_relation, fc_xid, fc_snapshot);
}
